September 27th, 2025 Update
@@ -30,7 +30,7 @@ def check_github_rate_limit(session):
|
||||
print(f"Error checking GitHub rate limit: {exception}")
|
||||
return False
|
||||
|
||||
def download_file(cancel_param, destination, progress_param, url, download_param, session, files=1, file_number=1):
|
||||
def download_file(cancel_param, destination, progress_param, url, download_param, session, offset_bytes=0, total_bytes=0):
|
||||
try:
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -57,8 +57,10 @@ def download_file(cancel_param, destination, progress_param, url, download_param
|
||||
temp_file.write(chunk)
|
||||
downloaded_size += len(chunk)
|
||||
|
||||
file_progress = downloaded_size / total_size
|
||||
overall_progress = ((file_number - 1) + file_progress) / files * 100
|
||||
if total_bytes:
|
||||
overall_progress = (offset_bytes + downloaded_size) / total_bytes * 100
|
||||
else:
|
||||
overall_progress = downloaded_size / total_size * 100
|
||||
|
||||
if overall_progress != 100:
|
||||
params_memory.put(progress_param, f"{overall_progress:.0f}%")
|
||||
@@ -98,10 +100,10 @@ def handle_error(destination, error_message, error, download_param, progress_par
|
||||
|
||||
def handle_request_error(error, destination, download_param, progress_param):
|
||||
error_map = {
|
||||
requests.ConnectionError: "Connection dropped",
|
||||
requests.HTTPError: lambda error: f"Server error ({error.response.status_code})" if error.response else "Server error",
|
||||
requests.RequestException: "Network request error. Check connection",
|
||||
requests.Timeout: "Download timed out"
|
||||
requests.exceptions.ConnectionError: "Connection dropped",
|
||||
requests.exceptions.HTTPError: lambda error: f"Server error ({error.response.status_code})" if error and getattr(error, "response", None) else "Server error",
|
||||
requests.exceptions.RequestException: "Network request error. Check connection",
|
||||
requests.exceptions.Timeout: "Download timed out",
|
||||
}
|
||||
|
||||
error_message = error_map.get(type(error), "Unexpected error")
|
||||
|
||||
@@ -7,24 +7,30 @@ import time
|
||||
import urllib.parse
|
||||
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.frogpilot.assets.download_functions import GITLAB_URL, download_file, get_repository_url, handle_error, handle_request_error, verify_download
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import delete_file
|
||||
from openpilot.frogpilot.common.frogpilot_variables import DEFAULT_CLASSIC_MODEL, DEFAULT_MODEL, DEFAULT_TINYGRAD_MODEL, MODELS_PATH, RESOURCES_REPO, TINYGRAD_FILES, \
|
||||
params, params_default, params_memory
|
||||
from openpilot.frogpilot.assets.download_functions import GITLAB_URL, download_file, get_remote_file_size, get_repository_url, handle_error, handle_request_error, verify_download
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import delete_file, extract_tar, load_json_file, update_json_file
|
||||
from openpilot.frogpilot.common.frogpilot_variables import DEFAULT_MODEL, MODELS_PATH, RESOURCES_REPO, TINYGRAD_FILES, params, params_default, params_memory, update_frogpilot_toggles
|
||||
|
||||
VERSION = "v15"
|
||||
VERSION = "v16"
|
||||
VERSION_PATH = MODELS_PATH / "model_version"
|
||||
|
||||
CANCEL_DOWNLOAD_PARAM = "CancelModelDownload"
|
||||
DOWNLOAD_PROGRESS_PARAM = "ModelDownloadProgress"
|
||||
MODEL_DOWNLOAD_PARAM = "ModelToDownload"
|
||||
MODEL_DOWNLOAD_ALL_PARAM = "DownloadAllModels"
|
||||
UPDATE_TINYGRAD_PARAM = "UpdateTinygrad"
|
||||
|
||||
DEFAULT_TINYGRAD_SIZE = 87746736
|
||||
TAR_FILE_NAME = f"Tinygrad_{VERSION}.tar.gz"
|
||||
|
||||
TINYGRAD_MODELD_PATH = Path(BASEDIR) / "frogpilot/tinygrad_modeld"
|
||||
TINYGRAD_REPO_PATH = Path(BASEDIR) / "tinygrad_repo"
|
||||
|
||||
class ModelManager:
|
||||
def __init__(self):
|
||||
def __init__(self, boot_run=False):
|
||||
self.downloading_model = False
|
||||
|
||||
self.available_models = (params.get("AvailableModels", encoding="utf-8") or "").split(",")
|
||||
@@ -32,29 +38,34 @@ class ModelManager:
|
||||
self.model_versions = (params.get("ModelVersions", encoding="utf-8") or "").split(",")
|
||||
|
||||
self.model_sizes_path = MODELS_PATH / "model_sizes.json"
|
||||
self.tinygrad_sizes_path = MODELS_PATH / "tinygrad_sizes.json"
|
||||
|
||||
self.model_sizes = load_json_file(self.model_sizes_path)
|
||||
self.tinygrad_sizes = load_json_file(self.tinygrad_sizes_path)
|
||||
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({"Accept-Language": "en"})
|
||||
self.session.headers.update({"User-Agent": "frogpilot-model-downloader/1.0 (https://github.com/FrogAi/FrogPilot)"})
|
||||
|
||||
def check_models(self, boot_run, repo_url):
|
||||
downloaded_models = [path for path in MODELS_PATH.iterdir() if path.is_file()]
|
||||
if boot_run:
|
||||
self.copy_default_model()
|
||||
self.validate_models()
|
||||
|
||||
def check_models(self, boot_run, repo_url):
|
||||
downloaded_models = [
|
||||
model for model in MODELS_PATH.iterdir()
|
||||
if (MODELS_PATH / f"{model}.thneed").is_file() or all((MODELS_PATH / f"{model}_{filename}").is_file() for filename, _ in TINYGRAD_FILES)
|
||||
]
|
||||
for model_file in downloaded_models:
|
||||
if model_file.name not in {self.model_sizes_path.name} and not any(model in model_file.name for model in set(self.available_models)):
|
||||
if not any(model in model_file.name for model in set(self.available_models)):
|
||||
print(f"Removing outdated model: {model_file}")
|
||||
delete_file(model_file)
|
||||
|
||||
for onnx_file in MODELS_PATH.glob("*.onnx"):
|
||||
if onnx_file.is_file():
|
||||
print(f"Deleting .onnx file: {onnx_file}")
|
||||
delete_file(onnx_file)
|
||||
|
||||
for tmp_file in MODELS_PATH.glob("tmp*"):
|
||||
if tmp_file.is_file():
|
||||
delete_file(tmp_file)
|
||||
|
||||
if params.get("Model", encoding="utf-8") not in self.available_models:
|
||||
if params.get("Model", encoding="utf-8").removesuffix("_default") not in self.available_models:
|
||||
params.put("Model", params_default.get("Model", encoding="utf-8"))
|
||||
|
||||
if not (not boot_run and params.get_bool("AutomaticallyDownloadModels")):
|
||||
@@ -65,46 +76,80 @@ class ModelManager:
|
||||
print("No model size data available. Skipping model checks...")
|
||||
return
|
||||
|
||||
local_model_sizes = self.load_model_sizes()
|
||||
|
||||
needs_download = False
|
||||
for model, version in zip(self.available_models, self.model_versions):
|
||||
if version in {"v1", "v2", "v3", "v4", "v5", "v6"}:
|
||||
need_to_update_models = False
|
||||
for model in self.available_models:
|
||||
if self.is_tinygrad_model(model):
|
||||
model_file = MODELS_PATH / f"{model}.thneed"
|
||||
if not model_file.is_file():
|
||||
needs_download = True
|
||||
need_to_update_models = True
|
||||
continue
|
||||
|
||||
expected_size = model_sizes.get(model_file.name)
|
||||
local_size = local_model_sizes.get(model_file.name)
|
||||
local_size = self.model_sizes.get(model_file.name)
|
||||
|
||||
if expected_size > 0 and local_size != expected_size:
|
||||
print(f"Model {model} is outdated. Deleting {model_file}...")
|
||||
delete_file(model_file)
|
||||
needs_download = True
|
||||
need_to_update_models = True
|
||||
else:
|
||||
model_missing = False
|
||||
model_outdated = False
|
||||
|
||||
if needs_download:
|
||||
for filename, _ in TINYGRAD_FILES:
|
||||
expected_file = MODELS_PATH / f"{model}_{filename}"
|
||||
if not expected_file.is_file():
|
||||
model_missing = True
|
||||
need_to_update_models = True
|
||||
break
|
||||
|
||||
for filename, _ in TINYGRAD_FILES:
|
||||
model_file = f"{model}_{filename}"
|
||||
|
||||
expected_size = model_sizes.get(model_file)
|
||||
local_size = self.model_sizes.get(model_file)
|
||||
|
||||
if expected_size > 0 and local_size != expected_size:
|
||||
model_outdated = True
|
||||
need_to_update_models = True
|
||||
break
|
||||
|
||||
if model_missing or model_outdated:
|
||||
print(f"Model {model} is either missing required files or outdated. Deleting...")
|
||||
for filename, _ in TINYGRAD_FILES:
|
||||
delete_file(MODELS_PATH / f"{model}_{filename}")
|
||||
|
||||
if need_to_update_models:
|
||||
params_memory.put_bool(MODEL_DOWNLOAD_ALL_PARAM, True)
|
||||
|
||||
def check_tinygrad(self, repo_url):
|
||||
tinygrad_url = f"{repo_url}/Tinygrad/{TAR_FILE_NAME}"
|
||||
|
||||
expected_size = get_remote_file_size(tinygrad_url, self.session)
|
||||
local_size = int(self.tinygrad_sizes.get(TAR_FILE_NAME, 0))
|
||||
|
||||
if expected_size > 0 and local_size != expected_size:
|
||||
print(f"Tinygrad version {VERSION} is outdated, expected_size: {expected_size}, local_size: {local_size}, flagging for update...")
|
||||
params.put_bool("TinygradUpdateAvailable", True)
|
||||
|
||||
def copy_default_model(self):
|
||||
classic_default_model_path = MODELS_PATH / f"{DEFAULT_CLASSIC_MODEL}.thneed"
|
||||
classic_default_model_path = MODELS_PATH / "wd-40.thneed"
|
||||
source_path = Path(__file__).parents[1] / "classic_modeld/models/supercombo.thneed"
|
||||
if source_path.is_file() and not classic_default_model_path.is_file():
|
||||
if source_path.is_file() and (not classic_default_model_path.is_file() or source_path.stat().st_size != classic_default_model_path.stat().st_size):
|
||||
shutil.copyfile(source_path, classic_default_model_path)
|
||||
print(f"Copied the classic default model from {source_path} to {classic_default_model_path}")
|
||||
self.update_model_size(classic_default_model_path)
|
||||
|
||||
default_model_path = MODELS_PATH / f"{DEFAULT_MODEL}.thneed"
|
||||
default_model_path = MODELS_PATH / "national-public-radio.thneed"
|
||||
source_path = Path(__file__).parents[2] / "selfdrive/modeld/models/supercombo.thneed"
|
||||
if source_path.is_file() and not default_model_path.is_file():
|
||||
if source_path.is_file() and (not default_model_path.is_file() or source_path.stat().st_size != default_model_path.stat().st_size):
|
||||
shutil.copyfile(source_path, default_model_path)
|
||||
print(f"Copied the default model from {source_path} to {default_model_path}")
|
||||
self.update_model_size(default_model_path)
|
||||
|
||||
for filename, description in TINYGRAD_FILES:
|
||||
source = TINYGRAD_MODELD_PATH / "models" / filename
|
||||
target = MODELS_PATH / f"{DEFAULT_TINYGRAD_MODEL}_{filename}"
|
||||
if source.is_file() and not target.is_file():
|
||||
target = MODELS_PATH / f"{DEFAULT_MODEL}_{filename}"
|
||||
if source.is_file() and (not target.is_file() or source.stat().st_size != target.stat().st_size):
|
||||
shutil.copyfile(source, target)
|
||||
print(f"Copied the tinygrad {description} from {source} to {target}")
|
||||
|
||||
@@ -121,16 +166,20 @@ class ModelManager:
|
||||
handle_error(None, "Download cancelled...", "Download cancelled...", MODEL_DOWNLOAD_ALL_PARAM, DOWNLOAD_PROGRESS_PARAM)
|
||||
return
|
||||
|
||||
already_downloaded = [model_file for model_file in MODELS_PATH.iterdir() if model_file.is_file() and model in model_file.name]
|
||||
if self.is_tinygrad_model(model):
|
||||
already_downloaded = (MODELS_PATH / f"{model}.thneed").is_file()
|
||||
else:
|
||||
already_downloaded = all((MODELS_PATH / f"{model}_{filename}").is_file() for filename, _ in TINYGRAD_FILES)
|
||||
|
||||
if already_downloaded:
|
||||
continue
|
||||
|
||||
print(f"Model {model} is not downloaded. Preparing to download...")
|
||||
params_memory.put(DOWNLOAD_PROGRESS_PARAM, f"Downloading \"{self.available_model_names[self.available_models.index(model)]}\"...")
|
||||
|
||||
self.download_model(model)
|
||||
|
||||
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "All models downloaded!")
|
||||
params_memory.remove(MODEL_DOWNLOAD_ALL_PARAM)
|
||||
|
||||
def download_model(self, model_to_download):
|
||||
self.downloading_model = True
|
||||
@@ -141,7 +190,7 @@ class ModelManager:
|
||||
self.downloading_model = False
|
||||
return
|
||||
|
||||
if self.model_versions[self.available_models.index(model_to_download)] in {"v1", "v2", "v3", "v4", "v5", "v6"}:
|
||||
if self.is_tinygrad_model(model_to_download):
|
||||
model_path = MODELS_PATH / f"{model_to_download}.thneed"
|
||||
model_url = f"{repo_url}/Models/{model_to_download}.thneed"
|
||||
|
||||
@@ -149,6 +198,8 @@ class ModelManager:
|
||||
download_file(CANCEL_DOWNLOAD_PARAM, model_path, DOWNLOAD_PROGRESS_PARAM, model_url, MODEL_DOWNLOAD_PARAM, self.session)
|
||||
|
||||
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
|
||||
delete_file(model_path)
|
||||
|
||||
handle_error(None, "Download cancelled...", "Download cancelled...", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
|
||||
self.downloading_model = False
|
||||
return
|
||||
@@ -156,8 +207,10 @@ class ModelManager:
|
||||
if verify_download(model_path, model_url, self.session):
|
||||
print(f"Model {model_to_download} downloaded and verified successfully!")
|
||||
self.update_model_size(model_path)
|
||||
|
||||
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Downloaded!")
|
||||
params_memory.remove(MODEL_DOWNLOAD_PARAM)
|
||||
|
||||
self.downloading_model = False
|
||||
return
|
||||
|
||||
@@ -166,6 +219,8 @@ class ModelManager:
|
||||
download_file(CANCEL_DOWNLOAD_PARAM, model_path, DOWNLOAD_PROGRESS_PARAM, fallback_url, MODEL_DOWNLOAD_PARAM, self.session)
|
||||
|
||||
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
|
||||
delete_file(model_path)
|
||||
|
||||
handle_error(None, "Download cancelled...", "Download cancelled...", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
|
||||
self.downloading_model = False
|
||||
return
|
||||
@@ -173,40 +228,132 @@ class ModelManager:
|
||||
if verify_download(model_path, fallback_url, self.session):
|
||||
print(f"Model {model_to_download} downloaded and verified successfully from GitLab!")
|
||||
self.update_model_size(model_path)
|
||||
|
||||
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Downloaded!")
|
||||
params_memory.remove(MODEL_DOWNLOAD_PARAM)
|
||||
|
||||
self.downloading_model = False
|
||||
else:
|
||||
handle_error(model_path, "Verification failed...", "GitLab verification failed", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
|
||||
self.downloading_model = False
|
||||
else:
|
||||
all_model_sizes = self.fetch_all_model_sizes(repo_url) or {}
|
||||
tinygrad_filenames = [f"{model_to_download}_{file_key}" for file_key, _ in TINYGRAD_FILES]
|
||||
|
||||
file_sizes = []
|
||||
file_sources = []
|
||||
|
||||
missing = [name for name in tinygrad_filenames if int(all_model_sizes.get(name, 0)) <= 0]
|
||||
if missing:
|
||||
handle_error(None, "Missing size metadata...", f"Sizes not found for: {', '.join(missing)}...", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
|
||||
self.downloading_model = False
|
||||
return
|
||||
|
||||
for filename in tinygrad_filenames:
|
||||
primary_url = f"{repo_url}/Models/compiled/{filename}"
|
||||
file_size = int(all_model_sizes.get(filename, 0))
|
||||
file_sizes.append(file_size)
|
||||
file_sources.append((primary_url, None))
|
||||
|
||||
downloaded_offset_bytes = 0
|
||||
known_file_sizes = [size for size in file_sizes if size > 0]
|
||||
total_model_bytes = sum(known_file_sizes) if len(known_file_sizes) == len(file_sizes) else 0
|
||||
|
||||
for (file_key, description), part_bytes, (primary_url, fallback_url) in zip(TINYGRAD_FILES, file_sizes, file_sources):
|
||||
filename = f"{model_to_download}_{file_key}"
|
||||
model_path = MODELS_PATH / filename
|
||||
|
||||
print(f"Downloading {description} for model: {model_to_download}")
|
||||
download_file(CANCEL_DOWNLOAD_PARAM, model_path, DOWNLOAD_PROGRESS_PARAM, primary_url, MODEL_DOWNLOAD_PARAM, self.session, offset_bytes=downloaded_offset_bytes, total_bytes=total_model_bytes)
|
||||
|
||||
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
|
||||
delete_file(model_path)
|
||||
|
||||
handle_error(None, "Download cancelled...", "Download cancelled...", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
|
||||
self.downloading_model = False
|
||||
return
|
||||
|
||||
if verify_download(model_path, primary_url, self.session):
|
||||
print(f"{description.capitalize()} for {model_to_download} downloaded and verified successfully!")
|
||||
if total_model_bytes:
|
||||
downloaded_offset_bytes += part_bytes
|
||||
continue
|
||||
|
||||
print(f"Verification failed for {filename}. Retrying from GitLab...")
|
||||
fallback_url = f"{GITLAB_URL}/Models/compiled/{filename}"
|
||||
download_file(CANCEL_DOWNLOAD_PARAM, model_path, DOWNLOAD_PROGRESS_PARAM, fallback_url, MODEL_DOWNLOAD_PARAM, self.session, offset_bytes=downloaded_offset_bytes, total_bytes=total_model_bytes)
|
||||
|
||||
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
|
||||
delete_file(model_path)
|
||||
|
||||
handle_error(None, "Download cancelled...", "Download cancelled...", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
|
||||
self.downloading_model = False
|
||||
return
|
||||
|
||||
if verify_download(model_path, fallback_url, self.session):
|
||||
print(f"{description.capitalize()} for {model_to_download} downloaded and verified successfully from GitLab!")
|
||||
if total_model_bytes:
|
||||
downloaded_offset_bytes += part_bytes
|
||||
else:
|
||||
handle_error(model_path, "Verification failed...", f"GitLab verification failed for {filename}", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
|
||||
self.downloading_model = False
|
||||
return
|
||||
|
||||
print(f"Updating model sizes for {model_to_download}...")
|
||||
for filename, _ in TINYGRAD_FILES:
|
||||
file_path = MODELS_PATH / f"{model_to_download}_{filename}"
|
||||
self.update_model_size(file_path)
|
||||
|
||||
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Downloaded!")
|
||||
params_memory.remove(MODEL_DOWNLOAD_PARAM)
|
||||
|
||||
self.downloading_model = False
|
||||
|
||||
def fetch_all_model_sizes(self, repo_url):
|
||||
if "github" in repo_url:
|
||||
api_url = f"https://api.github.com/repos/{RESOURCES_REPO}/contents?ref=Models"
|
||||
elif "gitlab" in repo_url:
|
||||
api_url = f"https://gitlab.com/api/v4/projects/{urllib.parse.quote_plus(RESOURCES_REPO)}/repository/tree?ref=Models"
|
||||
else:
|
||||
return {}
|
||||
is_github = "github" in repo_url
|
||||
is_gitlab = "gitlab" in repo_url
|
||||
repo_encoded = quote_plus(RESOURCES_REPO)
|
||||
|
||||
model_sizes = {}
|
||||
try:
|
||||
response = self.session.get(api_url)
|
||||
response.raise_for_status()
|
||||
def fetch_dir_sizes(api_url):
|
||||
sizes = {}
|
||||
print(f"Fetching model metadata: {api_url}")
|
||||
response = self.session.get(api_url, timeout=10)
|
||||
response.raise_for_status()
|
||||
content = response.json()
|
||||
|
||||
model_files = [file for file in response.json() if "." in file["name"]]
|
||||
model_files = [file for file in content if "." in file["name"]]
|
||||
|
||||
if "gitlab" in repo_url:
|
||||
model_sizes = {}
|
||||
for file in model_files:
|
||||
metadata_url = f"https://gitlab.com/api/v4/projects/{urllib.parse.quote_plus(RESOURCES_REPO)}/repository/files/{urllib.parse.quote_plus(file['path'])}/raw?ref=Models"
|
||||
metadata_response = self.session.head(metadata_url)
|
||||
metadata_response.raise_for_status()
|
||||
model_sizes[file["name"]] = int(metadata_response.headers.get("content-length", 0))
|
||||
return model_sizes
|
||||
if is_github:
|
||||
for file in model_files:
|
||||
sizes[file["name"]] = file.get("size", 0)
|
||||
else:
|
||||
for file in model_files:
|
||||
file_path = quote_plus(file["path"])
|
||||
metadata_url = f"https://gitlab.com/api/v4/projects/{repo_encoded}/repository/files/{file_path}/raw?ref=Models"
|
||||
head_response = self.session.head(metadata_url, timeout=10)
|
||||
if head_response.ok:
|
||||
sizes[file["name"]] = int(head_response.headers.get("content-length", 0))
|
||||
return sizes
|
||||
|
||||
if is_github:
|
||||
top_api_url = f"https://api.github.com/repos/{RESOURCES_REPO}/contents?ref=Models"
|
||||
version_api_url = f"https://api.github.com/repos/{RESOURCES_REPO}/contents/compiled?ref=Models"
|
||||
elif is_gitlab:
|
||||
top_api_url = f"https://gitlab.com/api/v4/projects/{repo_encoded}/repository/tree?ref=Models"
|
||||
version_api_url = f"https://gitlab.com/api/v4/projects/{repo_encoded}/repository/tree?path=compiled&ref=Models"
|
||||
else:
|
||||
return {file["name"]: file["size"] for file in model_files if "size" in file}
|
||||
print(f"Unsupported repository URL: {repo_url}")
|
||||
return model_sizes
|
||||
|
||||
except Exception as exception:
|
||||
handle_request_error(f"Failed to fetch model sizes from {'GitHub' if 'github' in repo_url else 'GitLab'}: {exception}", None, None, None)
|
||||
model_sizes.update(fetch_dir_sizes(top_api_url))
|
||||
model_sizes.update(fetch_dir_sizes(version_api_url))
|
||||
|
||||
return model_sizes
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
handle_request_error(f"Failed to fetch model sizes from {'GitHub' if is_github else 'GitLab'}: {e}", None, None, None)
|
||||
return {}
|
||||
|
||||
def fetch_models(self, url, repo_url, boot_run=False):
|
||||
@@ -218,15 +365,13 @@ class ModelManager:
|
||||
if model_info:
|
||||
self.update_model_params(model_info)
|
||||
self.check_models(boot_run, repo_url)
|
||||
self.check_tinygrad(repo_url)
|
||||
except Exception as exception:
|
||||
handle_request_error(exception, None, None, None)
|
||||
return []
|
||||
|
||||
def load_model_sizes(self):
|
||||
if self.model_sizes_path.is_file():
|
||||
with open(self.model_sizes_path) as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
def is_tinygrad_model(self, model):
|
||||
return self.model_versions[self.available_models.index(model)] in {"v1", "v2", "v3", "v4", "v5", "v6"}
|
||||
|
||||
def update_model_params(self, model_info):
|
||||
self.available_models = [model["id"] for model in model_info]
|
||||
@@ -250,9 +395,136 @@ class ModelManager:
|
||||
self.fetch_models(f"{repo_url}/Versions/model_names_{VERSION}.json", repo_url, boot_run)
|
||||
|
||||
def update_model_size(self, file_path):
|
||||
sizes = self.load_model_sizes()
|
||||
sizes[file_path.name] = file_path.stat().st_size
|
||||
|
||||
with open(self.model_sizes_path, "w") as f:
|
||||
json.dump(sizes, f, indent=2)
|
||||
self.model_sizes[file_path.name] = file_path.stat().st_size
|
||||
update_json_file(self.model_sizes_path, self.model_sizes)
|
||||
print(f"Updated size for {file_path.name} in {self.model_sizes_path.name}")
|
||||
|
||||
def update_tinygrad_size(self, file_path):
|
||||
self.tinygrad_sizes[TAR_FILE_NAME] = file_path.stat().st_size
|
||||
update_json_file(self.tinygrad_sizes_path, self.tinygrad_sizes)
|
||||
print(f"Updated size for {TAR_FILE_NAME} in {self.tinygrad_sizes_path.name}")
|
||||
|
||||
def update_tinygrad(self):
|
||||
repo_url = get_repository_url(self.session)
|
||||
if not repo_url:
|
||||
handle_error(None, "GitHub and GitLab are offline...", "Repository unavailable", None, None)
|
||||
return
|
||||
|
||||
primary_url = f"{repo_url}/Tinygrad/{TAR_FILE_NAME}"
|
||||
fallback_url = f"https://gitlab.com/{RESOURCES_REPO}/-/raw/Tinygrad/{TAR_FILE_NAME}"
|
||||
|
||||
tinygrad_tar_path = Path("/data/tmp/tinygrad.tar.gz")
|
||||
try:
|
||||
print(f"Attempting to download tinygrad from {primary_url}...")
|
||||
download_file(CANCEL_DOWNLOAD_PARAM, tinygrad_tar_path, DOWNLOAD_PROGRESS_PARAM, primary_url, UPDATE_TINYGRAD_PARAM, self.session)
|
||||
|
||||
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
|
||||
delete_file(tinygrad_tar_path)
|
||||
|
||||
handle_error(None, "Tinygrad update cancelled...", "Tinygrad update cancelled...", UPDATE_TINYGRAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
|
||||
params_memory.remove("CancelModelDownload")
|
||||
return
|
||||
|
||||
if not verify_download(tinygrad_tar_path, primary_url, self.session):
|
||||
print(f"Verification failed for {primary_url}. Retrying from GitLab...")
|
||||
download_file(CANCEL_DOWNLOAD_PARAM, tinygrad_tar_path, DOWNLOAD_PROGRESS_PARAM, fallback_url, UPDATE_TINYGRAD_PARAM, self.session)
|
||||
|
||||
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
|
||||
delete_file(tinygrad_tar_path)
|
||||
|
||||
handle_error(None, "Tinygrad update cancelled...", "Tinygrad update cancelled...", UPDATE_TINYGRAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
|
||||
params_memory.remove("CancelModelDownload")
|
||||
return
|
||||
|
||||
if not verify_download(tinygrad_tar_path, fallback_url, self.session):
|
||||
handle_error(tinygrad_tar_path, "Verification Failed", "Tinygrad verification failed", UPDATE_TINYGRAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
|
||||
return
|
||||
|
||||
print("Tinygrad downloaded successfully! Proceeding with installation...")
|
||||
self.update_tinygrad_size(tinygrad_tar_path)
|
||||
|
||||
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Installing...")
|
||||
|
||||
print("Deleting old tinygrad directories...")
|
||||
delete_file(TINYGRAD_MODELD_PATH)
|
||||
print(f"Removed {TINYGRAD_MODELD_PATH}")
|
||||
delete_file(TINYGRAD_REPO_PATH)
|
||||
print(f"Removed {TINYGRAD_REPO_PATH}")
|
||||
|
||||
extract_tar(tinygrad_tar_path, Path(BASEDIR))
|
||||
|
||||
print("Tinygrad update completed successfully!")
|
||||
|
||||
params.put_bool("TinygradUpdateAvailable", False)
|
||||
|
||||
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Updated!")
|
||||
params_memory.remove(UPDATE_TINYGRAD_PARAM)
|
||||
|
||||
self.update_tinygrad_models(repo_url)
|
||||
except Exception as exception:
|
||||
handle_error(tinygrad_tar_path, "Update Failed", f"An unexpected error occurred: {exception}", UPDATE_TINYGRAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
|
||||
|
||||
def update_tinygrad_models(self, repo_url=None):
|
||||
print("Updating old Tinygrad models...")
|
||||
|
||||
installed_tinygrad_models = set()
|
||||
for filename, _ in TINYGRAD_FILES:
|
||||
suffix = f"_{filename}"
|
||||
for file_path in MODELS_PATH.glob(f"*{suffix}"):
|
||||
model_name = file_path.name.rsplit(suffix, 1)[0]
|
||||
if model_name in set(self.available_models):
|
||||
installed_tinygrad_models.add(model_name)
|
||||
delete_file(file_path)
|
||||
|
||||
self.copy_default_model()
|
||||
|
||||
update_frogpilot_toggles()
|
||||
|
||||
if repo_url is None:
|
||||
return
|
||||
|
||||
current_model = params.get("Model", encoding="utf-8").removesuffix("_default")
|
||||
|
||||
models_to_redownload = [current_model]
|
||||
models_to_redownload += [model for model in sorted(installed_tinygrad_models) if model != current_model]
|
||||
|
||||
if DEFAULT_MODEL in models_to_redownload:
|
||||
models_to_redownload.remove(DEFAULT_MODEL)
|
||||
|
||||
if models_to_redownload:
|
||||
print(f"Redownloading the following models: {', '.join(models_to_redownload)}")
|
||||
self.fetch_models(f"{repo_url}/Versions/model_names_{VERSION}.json", repo_url, boot_run=True)
|
||||
|
||||
for model in models_to_redownload:
|
||||
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
|
||||
handle_error(None, "Download cancelled...", "Download cancelled...", MODEL_DOWNLOAD_ALL_PARAM, DOWNLOAD_PROGRESS_PARAM)
|
||||
return
|
||||
|
||||
params_memory.put(DOWNLOAD_PROGRESS_PARAM, f"Downloading \"{self.available_model_names[self.available_models.index(model)]}\"...")
|
||||
self.download_model(model)
|
||||
else:
|
||||
print("No previously installed tinygrad models to redownload")
|
||||
|
||||
update_frogpilot_toggles()
|
||||
|
||||
def validate_models(self):
|
||||
current = params.get("Model", encoding="utf-8")
|
||||
default = params_default.get("Model", encoding="utf-8")
|
||||
|
||||
if current.endswith("_default") and current != default:
|
||||
print(f"Model '{current}' does not match default '{default}', resetting...")
|
||||
params.put("Model", default)
|
||||
|
||||
if VERSION_PATH.is_file():
|
||||
version_name = VERSION_PATH.read_text().strip()
|
||||
if version_name != VERSION or int(self.tinygrad_sizes.get(TAR_FILE_NAME, 0)) == 0:
|
||||
self.update_tinygrad_models()
|
||||
|
||||
self.tinygrad_sizes[TAR_FILE_NAME] = DEFAULT_TINYGRAD_SIZE
|
||||
update_json_file(self.tinygrad_sizes_path, self.tinygrad_sizes)
|
||||
print(f"Updated size for {TAR_FILE_NAME} in {self.tinygrad_sizes_path.name}")
|
||||
|
||||
params.remove("TinygradUpdateAvailable")
|
||||
|
||||
VERSION_PATH.write_text(VERSION)
|
||||
print(f"Updated {VERSION_PATH} to {VERSION}")
|
||||
|
||||
|
After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 479 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 454 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 309 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 549 KiB |
@@ -0,0 +1 @@
|
||||
../../../selfdrive/assets/images
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 957 B |
@@ -0,0 +1 @@
|
||||
../../../selfdrive/assets/sounds
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 48 B |
@@ -0,0 +1 @@
|
||||
../../../../selfdrive/assets/img_chffr_wheel.png
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 48 B |
@@ -8,10 +8,11 @@ import shutil
|
||||
from datetime import date, timedelta
|
||||
from dateutil import easter
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
from openpilot.frogpilot.assets.download_functions import GITLAB_URL, download_file, get_repository_url, handle_error, handle_request_error, verify_download
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import delete_file, extract_zip
|
||||
from openpilot.frogpilot.common.frogpilot_variables import ACTIVE_THEME_PATH, RANDOM_EVENTS_PATH, RESOURCES_REPO, THEME_SAVE_PATH, params, params_memory, update_frogpilot_toggles
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import delete_file, extract_zip, load_json_file, update_json_file
|
||||
from openpilot.frogpilot.common.frogpilot_variables import ACTIVE_THEME_PATH, RANDOM_EVENTS_PATH, RESOURCES_REPO, THEME_SAVE_PATH, params, params_memory
|
||||
|
||||
CANCEL_DOWNLOAD_PARAM = "CancelThemeDownload"
|
||||
DOWNLOAD_PROGRESS_PARAM = "ThemeDownloadProgress"
|
||||
@@ -35,184 +36,17 @@ HOLIDAY_SLUGS = {
|
||||
"christmas_week": "Christmas"
|
||||
}
|
||||
|
||||
def calculate_thanksgiving(year):
|
||||
november_first = date(year, 11, 1)
|
||||
days_to_thursday = (3 - november_first.weekday()) % 7
|
||||
first_thursday = november_first + timedelta(days=days_to_thursday)
|
||||
return first_thursday + timedelta(days=21)
|
||||
|
||||
def get_full_themes():
|
||||
theme_packs_path = THEME_SAVE_PATH / "theme_packs"
|
||||
if not theme_packs_path.exists():
|
||||
return []
|
||||
|
||||
valid_themes = set()
|
||||
for theme_directory in theme_packs_path.iterdir():
|
||||
if not theme_directory.is_dir():
|
||||
continue
|
||||
|
||||
base_name = theme_directory.name.replace("-animated", "")
|
||||
|
||||
animated_path = theme_packs_path / f"{base_name}-animated"
|
||||
base_path = theme_packs_path / base_name
|
||||
|
||||
base_valid = all((base_path / asset).is_dir() for asset in {"colors", "sounds"})
|
||||
animated_icons_exist = (animated_path / "icons").is_dir()
|
||||
base_icons_exist = (base_path / "icons").is_dir()
|
||||
|
||||
if base_valid and (animated_icons_exist or base_icons_exist):
|
||||
if animated_icons_exist:
|
||||
valid_themes.add(f"{base_name}-animated")
|
||||
else:
|
||||
valid_themes.add(base_name)
|
||||
|
||||
return sorted(valid_themes)
|
||||
|
||||
def get_holiday_theme_dates(year):
|
||||
return {
|
||||
"new_years": date(year, 1, 1),
|
||||
"valentines_day": date(year, 2, 14),
|
||||
"st_patricks_day": date(year, 3, 17),
|
||||
"world_frog_day": date(year, 3, 20),
|
||||
"april_fools": date(year, 4, 1),
|
||||
"easter_week": easter.easter(year),
|
||||
"may_the_fourth": date(year, 5, 4),
|
||||
"cinco_de_mayo": date(year, 5, 5),
|
||||
"stitch_day": date(year, 6, 26),
|
||||
"fourth_of_july": date(year, 7, 4),
|
||||
"halloween_week": date(year, 10, 31),
|
||||
"thanksgiving_week": calculate_thanksgiving(year),
|
||||
"christmas_week": date(year, 12, 21)
|
||||
}
|
||||
|
||||
def randomize_distance_icons(available_themes, selected_theme):
|
||||
theme_packs_path = THEME_SAVE_PATH / "theme_packs"
|
||||
if not theme_packs_path.exists():
|
||||
return "stock"
|
||||
|
||||
candidates = []
|
||||
for theme_pack in theme_packs_path.iterdir():
|
||||
if not theme_pack.is_dir():
|
||||
continue
|
||||
|
||||
distance_icons_dir = theme_pack / "distance_icons"
|
||||
if not distance_icons_dir.is_dir():
|
||||
continue
|
||||
|
||||
icon_name = theme_pack.name.lower()
|
||||
|
||||
theme_association = [theme for theme in available_themes if theme.replace("-animated", "") in icon_name]
|
||||
if theme_association and selected_theme not in icon_name:
|
||||
continue
|
||||
|
||||
weight = 5 if selected_theme in icon_name else 1
|
||||
candidates.extend([theme_pack.name] * weight)
|
||||
|
||||
return random.choice(candidates) if candidates else "stock"
|
||||
|
||||
def randomize_theme_asset(available_themes):
|
||||
if not available_themes:
|
||||
return "stock"
|
||||
|
||||
return random.choice(available_themes)
|
||||
|
||||
def randomize_wheel_image(available_themes, selected_theme):
|
||||
steering_wheels_path = THEME_SAVE_PATH / "steering_wheels"
|
||||
if not steering_wheels_path.exists():
|
||||
return "stock"
|
||||
|
||||
candidates = []
|
||||
for wheel_file in steering_wheels_path.iterdir():
|
||||
if not wheel_file.is_file():
|
||||
continue
|
||||
|
||||
name = wheel_file.stem.lower()
|
||||
|
||||
theme_association = [theme for theme in available_themes if theme.replace("-animated", "") in name]
|
||||
if theme_association and selected_theme not in name:
|
||||
continue
|
||||
|
||||
weight = 5 if selected_theme in name else 1
|
||||
candidates.extend([wheel_file.stem] * weight)
|
||||
|
||||
return random.choice(candidates) if candidates else "stock"
|
||||
|
||||
def update_theme_asset(asset_type, theme, holiday_theme):
|
||||
save_location = ACTIVE_THEME_PATH / asset_type
|
||||
|
||||
if holiday_theme != "stock":
|
||||
asset_location = HOLIDAY_THEME_PATH / holiday_theme / asset_type
|
||||
elif theme in HOLIDAY_SLUGS:
|
||||
asset_location = HOLIDAY_THEME_PATH / theme / asset_type
|
||||
elif f"{theme}_week" in HOLIDAY_SLUGS:
|
||||
asset_location = HOLIDAY_THEME_PATH / f"{theme}_week" / asset_type
|
||||
else:
|
||||
asset_location = THEME_SAVE_PATH / "theme_packs" / theme / asset_type
|
||||
|
||||
if not asset_location.exists() or theme == "stock":
|
||||
if (STOCKOP_THEME_PATH / asset_type).is_dir():
|
||||
asset_location = STOCKOP_THEME_PATH / asset_type
|
||||
print(f"Using the stock {asset_type[:-1]} instead")
|
||||
else:
|
||||
if save_location.exists() or save_location.is_symlink():
|
||||
if save_location.is_symlink() or save_location.is_file():
|
||||
save_location.unlink()
|
||||
elif save_location.is_dir():
|
||||
shutil.rmtree(save_location)
|
||||
print(f"Using the stock {asset_type[:-1]} instead")
|
||||
return
|
||||
|
||||
if save_location.exists() or save_location.is_symlink():
|
||||
if save_location.is_symlink() or save_location.is_file():
|
||||
save_location.unlink()
|
||||
elif save_location.is_dir():
|
||||
shutil.rmtree(save_location)
|
||||
|
||||
save_location.parent.mkdir(parents=True, exist_ok=True)
|
||||
save_location.symlink_to(asset_location, target_is_directory=True)
|
||||
print(f"Linked {save_location} to {asset_location}")
|
||||
|
||||
def update_wheel_image(image, holiday_theme="stock", random_event=True):
|
||||
wheel_save_location = ACTIVE_THEME_PATH / "steering_wheel"
|
||||
|
||||
if holiday_theme != "stock":
|
||||
wheel_location = HOLIDAY_THEME_PATH / holiday_theme / "steering_wheel"
|
||||
elif random_event:
|
||||
wheel_location = RANDOM_EVENTS_PATH / "steering_wheels"
|
||||
elif image == "stock":
|
||||
wheel_location = STOCKOP_THEME_PATH / "steering_wheel"
|
||||
elif image in HOLIDAY_SLUGS:
|
||||
wheel_location = HOLIDAY_THEME_PATH / image / "steering_wheel"
|
||||
elif f"{image}_week" in HOLIDAY_SLUGS:
|
||||
wheel_location = HOLIDAY_THEME_PATH / f"{image}_week" / "steering_wheel"
|
||||
else:
|
||||
wheel_location = THEME_SAVE_PATH / "steering_wheels"
|
||||
|
||||
if not wheel_location.exists():
|
||||
wheel_location = STOCKOP_THEME_PATH / "steering_wheel"
|
||||
print("Using the stock steering wheel instead")
|
||||
|
||||
if wheel_save_location.exists():
|
||||
if wheel_save_location.is_symlink():
|
||||
wheel_save_location.unlink()
|
||||
elif wheel_save_location.is_dir():
|
||||
shutil.rmtree(wheel_save_location)
|
||||
|
||||
wheel_save_location.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
image_name = image.replace(" ", "_").lower()
|
||||
matching_files = [images for images in wheel_location.iterdir() if images.stem.lower() in {image_name, "wheel"}]
|
||||
if matching_files:
|
||||
source_file = matching_files[0]
|
||||
destination_file = wheel_save_location / f"wheel{source_file.suffix}"
|
||||
|
||||
if destination_file.exists():
|
||||
destination_file.unlink()
|
||||
destination_file.symlink_to(source_file)
|
||||
print(f"Linked {destination_file} to {source_file}")
|
||||
THEME_COMPONENT_PARAMS = {
|
||||
"colors": "ColorToDownload",
|
||||
"distance_icons": "DistanceIconToDownload",
|
||||
"icons": "IconToDownload",
|
||||
"signals": "SignalToDownload",
|
||||
"sounds": "SoundToDownload",
|
||||
"steering_wheels": "WheelToDownload"
|
||||
}
|
||||
|
||||
class ThemeManager:
|
||||
def __init__(self):
|
||||
def __init__(self, boot_run=False):
|
||||
self.downloading_theme = False
|
||||
self.theme_updated = False
|
||||
|
||||
@@ -220,24 +54,365 @@ class ThemeManager:
|
||||
|
||||
self.previous_asset_mappings = {}
|
||||
|
||||
self.theme_sizes_path = THEME_SAVE_PATH / "theme_sizes.json"
|
||||
|
||||
self.theme_sizes = load_json_file(self.theme_sizes_path)
|
||||
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({"Accept-Language": "en"})
|
||||
self.session.headers.update({"User-Agent": "frogpilot-theme-downloader/1.0 (https://github.com/FrogAi/FrogPilot)"})
|
||||
|
||||
if boot_run:
|
||||
self.copy_default_theme()
|
||||
|
||||
@staticmethod
|
||||
def calculate_thanksgiving(year):
|
||||
november_first = date(year, 11, 1)
|
||||
days_to_thursday = (3 - november_first.weekday()) % 7
|
||||
first_thursday = november_first + timedelta(days=days_to_thursday)
|
||||
return first_thursday + timedelta(days=21)
|
||||
|
||||
@staticmethod
|
||||
def copy_default_theme():
|
||||
world_frog_day_theme_path = HOLIDAY_THEME_PATH / "world_frog_day"
|
||||
|
||||
for theme_subfolder_name, save_subfolder_path in [
|
||||
("colors", "theme_packs/frog/colors"),
|
||||
("distance_icons", "theme_packs/frog-animated/distance_icons"),
|
||||
("icons", "theme_packs/frog-animated/icons"),
|
||||
("signals", "theme_packs/frog/signals"),
|
||||
("sounds", "theme_packs/frog/sounds"),
|
||||
]:
|
||||
source_folder_path = world_frog_day_theme_path / theme_subfolder_name
|
||||
destination_folder_path = THEME_SAVE_PATH / save_subfolder_path
|
||||
destination_folder_path.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copytree(source_folder_path, destination_folder_path, dirs_exist_ok=True)
|
||||
|
||||
steering_wheel_image_path = world_frog_day_theme_path / "steering_wheel/wheel.png"
|
||||
steering_wheel_save_path = THEME_SAVE_PATH / "steering_wheels/frog.png"
|
||||
steering_wheel_save_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(steering_wheel_image_path, steering_wheel_save_path)
|
||||
|
||||
def download_theme(self, theme_component, theme_name, asset_param, frogpilot_toggles):
|
||||
self.downloading_theme = True
|
||||
|
||||
repo_url = get_repository_url(self.session)
|
||||
if not repo_url:
|
||||
handle_error(None, "GitHub and GitLab are offline...", "Repository unavailable", asset_param, DOWNLOAD_PROGRESS_PARAM)
|
||||
self.downloading_theme = False
|
||||
return
|
||||
|
||||
if theme_component == "distance_icons":
|
||||
download_link = f"{repo_url}/Distance-Icons/{theme_name}"
|
||||
download_path = THEME_SAVE_PATH / "theme_packs" / theme_name / theme_component
|
||||
extensions = [".zip"]
|
||||
elif theme_component == "steering_wheels":
|
||||
download_link = f"{repo_url}/Steering-Wheels/{theme_name}"
|
||||
download_path = THEME_SAVE_PATH / theme_component / theme_name
|
||||
extensions = [".gif", ".png"]
|
||||
else:
|
||||
download_link = f"{repo_url}/Themes/{theme_name}/{theme_component}"
|
||||
download_path = THEME_SAVE_PATH / "theme_packs" / theme_name / theme_component
|
||||
extensions = [".zip"]
|
||||
|
||||
for extension in extensions:
|
||||
theme_path = download_path.with_suffix(extension)
|
||||
theme_url = download_link + extension
|
||||
|
||||
delete_file(theme_path)
|
||||
|
||||
print(f"Downloading theme from GitHub: {theme_name}")
|
||||
download_file(CANCEL_DOWNLOAD_PARAM, theme_path, DOWNLOAD_PROGRESS_PARAM, theme_url, asset_param, self.session)
|
||||
|
||||
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
|
||||
delete_file(theme_path)
|
||||
handle_error(None, "Download cancelled...", "Download cancelled...", asset_param, DOWNLOAD_PROGRESS_PARAM)
|
||||
|
||||
self.downloading_theme = False
|
||||
return
|
||||
|
||||
if verify_download(theme_path, theme_url, self.session):
|
||||
print(f"Theme {theme_name} downloaded and verified successfully from GitHub!")
|
||||
self.update_theme_size(theme_component, theme_name, theme_path.stat().st_size)
|
||||
|
||||
if extension == ".zip":
|
||||
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Unpacking theme...")
|
||||
extract_zip(theme_path, download_path)
|
||||
|
||||
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Downloaded!")
|
||||
params_memory.remove(asset_param)
|
||||
|
||||
self.downloading_theme = False
|
||||
|
||||
self.update_themes(frogpilot_toggles)
|
||||
return
|
||||
elif self.handle_verification_failure(extension, theme_component, theme_name, asset_param, theme_path, download_path, frogpilot_toggles):
|
||||
return
|
||||
|
||||
handle_error(download_path, "Download failed...", "Download failed...", asset_param, DOWNLOAD_PROGRESS_PARAM)
|
||||
self.downloading_theme = False
|
||||
|
||||
def fetch_assets(self, repo_url, frogpilot_toggles):
|
||||
is_github = "github" in repo_url
|
||||
is_gitlab = "gitlab" in repo_url
|
||||
|
||||
repo_encoded = quote_plus(RESOURCES_REPO)
|
||||
|
||||
assets = {"themes": {}, "wheels": []}
|
||||
try:
|
||||
def list_files(branch):
|
||||
if is_github:
|
||||
response = self.session.get(f"https://api.github.com/repos/{RESOURCES_REPO}/git/trees/{branch}?recursive=1", timeout=10)
|
||||
response.raise_for_status()
|
||||
return [
|
||||
{
|
||||
"path": item.get("path", ""),
|
||||
"name": Path(item.get("path", "")).name,
|
||||
"type": item.get("type"),
|
||||
"size": item.get("size", 0),
|
||||
}
|
||||
for item in response.json().get("tree", [])
|
||||
if item.get("type") == "blob"
|
||||
]
|
||||
if is_gitlab:
|
||||
response = self.session.get(f"https://gitlab.com/api/v4/projects/{repo_encoded}/repository/tree?ref={branch}&recursive=true", timeout=10)
|
||||
response.raise_for_status()
|
||||
return [
|
||||
{
|
||||
"path": item.get("path", ""),
|
||||
"name": item.get("name", ""),
|
||||
"type": item.get("type"),
|
||||
"size": 0,
|
||||
}
|
||||
for item in response.json()
|
||||
if item.get("type") in ("blob", "file")
|
||||
]
|
||||
print(f"Unsupported repository URL: {repo_url}")
|
||||
return []
|
||||
|
||||
def file_size(branch, path, fallback):
|
||||
if is_github:
|
||||
return int(fallback or 0)
|
||||
response = self.session.head(f"https://gitlab.com/api/v4/projects/{repo_encoded}/repository/files/{quote_plus(path)}/raw?ref={branch}", timeout=10)
|
||||
return int(response.headers.get("content-length", 0)) if response.ok else 0
|
||||
|
||||
for branch in ["Distance-Icons", "Steering-Wheels"]:
|
||||
for item in list_files(branch):
|
||||
if item.get("type") not in ("file", "blob"):
|
||||
continue
|
||||
|
||||
path = item["path"]
|
||||
size = file_size(branch, path, item.get("size", 0))
|
||||
|
||||
if branch == "Steering-Wheels":
|
||||
assets["wheels"].append(path)
|
||||
theme_name = Path(path).stem
|
||||
local_files = list((THEME_SAVE_PATH / "steering_wheels").glob(f"{theme_name}.*"))
|
||||
if local_files and size > 0:
|
||||
local_size = self.theme_sizes.get("wheels", {}).get(theme_name)
|
||||
if local_size != size:
|
||||
self.download_theme("steering_wheels", theme_name, THEME_COMPONENT_PARAMS["steering_wheels"], frogpilot_toggles)
|
||||
|
||||
elif branch == "Distance-Icons":
|
||||
component_name = "distance_icons"
|
||||
theme_name = Path(path).stem
|
||||
assets["themes"].setdefault(theme_name, set()).add(component_name)
|
||||
|
||||
local_path = THEME_SAVE_PATH / "theme_packs" / theme_name / component_name
|
||||
if local_path.exists() and size > 0:
|
||||
local_size = self.theme_sizes.get("themes", {}).get(theme_name, {}).get(component_name)
|
||||
if local_size != size:
|
||||
self.download_theme(component_name, theme_name, THEME_COMPONENT_PARAMS[component_name], frogpilot_toggles)
|
||||
|
||||
branch = "Themes"
|
||||
for item in list_files(branch):
|
||||
if item.get("type") not in ("file", "blob") or "/" not in item["path"]:
|
||||
continue
|
||||
|
||||
expected_size = file_size(branch, item["path"], item.get("size", 0))
|
||||
|
||||
theme_name, sub_path = item["path"].split("/", 1)
|
||||
theme_path = sub_path.lower()
|
||||
|
||||
for key in ("colors", "icons", "signals", "sounds"):
|
||||
if key in theme_path:
|
||||
assets["themes"].setdefault(theme_name, set()).add(key)
|
||||
|
||||
local_path = THEME_SAVE_PATH / "theme_packs" / theme_name / key
|
||||
if local_path.exists():
|
||||
local_size = self.theme_sizes.get("themes", {}).get(theme_name, {}).get(key)
|
||||
if local_size != expected_size:
|
||||
print(f"{key} {theme_name} is outdated, redownloading...")
|
||||
self.download_theme(key, theme_name, THEME_COMPONENT_PARAMS[key], frogpilot_toggles)
|
||||
break
|
||||
|
||||
assets["themes"] = {key: sorted(list(value)) for key, value in assets["themes"].items()}
|
||||
assets["wheels"].sort()
|
||||
return assets
|
||||
|
||||
except requests.exceptions.RequestException as error:
|
||||
print(f"Request failed: {error}")
|
||||
handle_request_error(f"Failed to fetch theme sizes from {'GitHub' if is_github else 'GitLab'}: {error}", None, None, None)
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def format_name(name, component):
|
||||
base = Path(name).stem
|
||||
creator = ""
|
||||
if "~" in base:
|
||||
base, creator = base.split("~", 1)
|
||||
|
||||
parts = base.replace("_", "-").split("-")
|
||||
capitalized_parts = [part.capitalize() for part in parts if part]
|
||||
|
||||
if len(capitalized_parts) > 1 and component != "steering_wheels":
|
||||
display = f"{capitalized_parts[0]} ({' '.join(capitalized_parts[1:])})"
|
||||
else:
|
||||
display = " ".join(capitalized_parts)
|
||||
|
||||
if creator:
|
||||
return f"{display} - by: {creator}"
|
||||
return display
|
||||
|
||||
@staticmethod
|
||||
def get_full_themes():
|
||||
theme_packs_path = THEME_SAVE_PATH / "theme_packs"
|
||||
if not theme_packs_path.exists():
|
||||
return []
|
||||
|
||||
valid_themes = set()
|
||||
for theme_directory in theme_packs_path.iterdir():
|
||||
if not theme_directory.is_dir():
|
||||
continue
|
||||
|
||||
base_name = theme_directory.name.replace("-animated", "")
|
||||
|
||||
animated_path = theme_packs_path / f"{base_name}-animated"
|
||||
base_path = theme_packs_path / base_name
|
||||
|
||||
base_valid = all((base_path / asset).is_dir() for asset in {"colors", "sounds"})
|
||||
animated_icons_exist = (animated_path / "icons").is_dir()
|
||||
base_icons_exist = (base_path / "icons").is_dir()
|
||||
|
||||
if base_valid and (animated_icons_exist or base_icons_exist):
|
||||
if animated_icons_exist:
|
||||
valid_themes.add(f"{base_name}-animated")
|
||||
else:
|
||||
valid_themes.add(base_name)
|
||||
|
||||
return sorted(valid_themes)
|
||||
|
||||
@staticmethod
|
||||
def get_holiday_theme_dates(year):
|
||||
return {
|
||||
"new_years": date(year, 1, 1),
|
||||
"valentines_day": date(year, 2, 14),
|
||||
"st_patricks_day": date(year, 3, 17),
|
||||
"world_frog_day": date(year, 3, 20),
|
||||
"april_fools": date(year, 4, 1),
|
||||
"easter_week": easter.easter(year),
|
||||
"may_the_fourth": date(year, 5, 4),
|
||||
"cinco_de_mayo": date(year, 5, 5),
|
||||
"stitch_day": date(year, 6, 26),
|
||||
"fourth_of_july": date(year, 7, 4),
|
||||
"halloween_week": date(year, 10, 31),
|
||||
"thanksgiving_week": ThemeManager.calculate_thanksgiving(year),
|
||||
"christmas_week": date(year, 12, 21)
|
||||
}
|
||||
|
||||
def handle_verification_failure(self, extension, theme_component, theme_name, asset_param, theme_path, download_path, frogpilot_toggles):
|
||||
if theme_component == "distance_icons":
|
||||
download_link = f"{GITLAB_URL}/Distance-Icons/{theme_name}"
|
||||
elif theme_component == "steering_wheels":
|
||||
download_link = f"{GITLAB_URL}/Steering-Wheels/{theme_name}"
|
||||
else:
|
||||
download_link = f"{GITLAB_URL}/Themes/{theme_name}/{theme_component}"
|
||||
|
||||
delete_file(theme_path)
|
||||
|
||||
theme_url = download_link + extension
|
||||
print(f"Downloading theme from GitLab: {theme_name}")
|
||||
download_file(CANCEL_DOWNLOAD_PARAM, theme_path, DOWNLOAD_PROGRESS_PARAM, theme_url, asset_param, self.session)
|
||||
|
||||
if verify_download(theme_path, theme_url, self.session):
|
||||
print(f"Theme {theme_name} downloaded and verified successfully from GitLab!")
|
||||
self.update_theme_size(theme_component, theme_name, theme_path.stat().st_size)
|
||||
|
||||
if extension == ".zip":
|
||||
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Unpacking theme...")
|
||||
extract_zip(theme_path, download_path)
|
||||
|
||||
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Downloaded!")
|
||||
params_memory.remove(asset_param)
|
||||
|
||||
self.downloading_theme = False
|
||||
|
||||
self.update_themes(frogpilot_toggles)
|
||||
return True
|
||||
|
||||
handle_error(None, "Download failed...", "Download failed...", asset_param, DOWNLOAD_PROGRESS_PARAM)
|
||||
self.downloading_theme = False
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def is_within_week_of(target_date, current_date):
|
||||
start_of_week = target_date - timedelta(days=target_date.weekday())
|
||||
return start_of_week <= current_date < target_date
|
||||
|
||||
def update_holiday(self):
|
||||
current_date = date.today()
|
||||
@staticmethod
|
||||
def randomize_distance_icons(available_themes, selected_theme):
|
||||
theme_packs_path = THEME_SAVE_PATH / "theme_packs"
|
||||
if not theme_packs_path.exists():
|
||||
return "stock"
|
||||
|
||||
holidays = get_holiday_theme_dates(current_date.year)
|
||||
for holiday, holiday_date in holidays.items():
|
||||
if (holiday.endswith("_week") and self.is_within_week_of(holiday_date, current_date)) or (current_date == holiday_date):
|
||||
return holiday
|
||||
candidates = []
|
||||
for theme_pack in theme_packs_path.iterdir():
|
||||
if not theme_pack.is_dir():
|
||||
continue
|
||||
|
||||
return "stock"
|
||||
distance_icons_dir = theme_pack / "distance_icons"
|
||||
if not distance_icons_dir.is_dir():
|
||||
continue
|
||||
|
||||
icon_name = theme_pack.name.lower()
|
||||
|
||||
theme_association = [theme for theme in available_themes if theme.replace("-animated", "") in icon_name]
|
||||
if theme_association and selected_theme not in icon_name:
|
||||
continue
|
||||
|
||||
weight = 5 if selected_theme in icon_name else 1
|
||||
candidates.extend([theme_pack.name] * weight)
|
||||
|
||||
return random.choice(candidates) if candidates else "stock"
|
||||
|
||||
@staticmethod
|
||||
def randomize_theme_asset(available_themes):
|
||||
if not available_themes:
|
||||
return "stock"
|
||||
|
||||
return random.choice(available_themes)
|
||||
|
||||
@staticmethod
|
||||
def randomize_wheel_image(available_themes, selected_theme):
|
||||
steering_wheels_path = THEME_SAVE_PATH / "steering_wheels"
|
||||
if not steering_wheels_path.exists():
|
||||
return "stock"
|
||||
|
||||
candidates = []
|
||||
for wheel_file in steering_wheels_path.iterdir():
|
||||
if not wheel_file.is_file():
|
||||
continue
|
||||
|
||||
name = wheel_file.stem.lower()
|
||||
|
||||
theme_association = [theme for theme in available_themes if theme.replace("-animated", "") in name]
|
||||
if theme_association and selected_theme not in name:
|
||||
continue
|
||||
|
||||
weight = 5 if selected_theme in name else 1
|
||||
candidates.extend([wheel_file.stem] * weight)
|
||||
|
||||
return random.choice(candidates) if candidates else "stock"
|
||||
|
||||
def update_active_theme(self, time_validated, frogpilot_toggles, boot_run=False, randomize_theme=False):
|
||||
if time_validated and frogpilot_toggles.holiday_themes:
|
||||
@@ -255,16 +430,16 @@ class ThemeManager:
|
||||
"wheel_image": ("wheel_image", self.holiday_theme)
|
||||
}
|
||||
elif (boot_run or randomize_theme) and frogpilot_toggles.random_themes:
|
||||
available_themes = get_full_themes()
|
||||
selected_theme = randomize_theme_asset(available_themes)
|
||||
available_themes = self.get_full_themes()
|
||||
selected_theme = self.randomize_theme_asset(available_themes)
|
||||
|
||||
asset_mappings = {
|
||||
"color_scheme": ("colors", selected_theme.replace("-animated", "")),
|
||||
"distance_icons": ("distance_icons", randomize_distance_icons(available_themes, selected_theme.replace("-animated", ""))),
|
||||
"distance_icons": ("distance_icons", self.randomize_distance_icons(available_themes, selected_theme.replace("-animated", ""))),
|
||||
"icon_pack": ("icons", selected_theme),
|
||||
"sound_pack": ("sounds", selected_theme.replace("-animated", "")),
|
||||
"turn_signal_pack": ("signals", selected_theme.replace("-animated", "")),
|
||||
"wheel_image": ("wheel_image", randomize_wheel_image(available_themes, selected_theme.replace("-animated", "")))
|
||||
"wheel_image": ("wheel_image", self.randomize_wheel_image(available_themes, selected_theme.replace("-animated", "")))
|
||||
}
|
||||
|
||||
elif not frogpilot_toggles.random_themes:
|
||||
@@ -284,150 +459,55 @@ class ThemeManager:
|
||||
print(f"Updating {asset}: {asset_type} with value {current_value}")
|
||||
|
||||
if asset_type == "wheel_image":
|
||||
update_wheel_image(current_value, self.holiday_theme, random_event=False)
|
||||
self.update_wheel_image(current_value, boot_run=boot_run)
|
||||
else:
|
||||
update_theme_asset(asset_type, current_value, self.holiday_theme)
|
||||
self.update_theme_asset(asset_type, current_value, boot_run=boot_run)
|
||||
|
||||
self.previous_asset_mappings = asset_mappings
|
||||
|
||||
self.theme_updated = True
|
||||
|
||||
def handle_verification_failure(self, ext, theme_component, theme_name, theme_param, theme_path, download_path):
|
||||
if theme_component == "steering_wheels":
|
||||
download_link = f"{GITLAB_URL}/Steering-Wheels/{theme_name}"
|
||||
def update_holiday(self):
|
||||
current_date = date.today()
|
||||
|
||||
holidays = self.get_holiday_theme_dates(current_date.year)
|
||||
for holiday, holiday_date in holidays.items():
|
||||
if (holiday.endswith("_week") and self.is_within_week_of(holiday_date, current_date)) or (current_date == holiday_date):
|
||||
return holiday
|
||||
|
||||
return "stock"
|
||||
|
||||
def update_theme_asset(self, asset_type, theme, boot_run=False):
|
||||
save_location = ACTIVE_THEME_PATH / asset_type
|
||||
|
||||
if self.holiday_theme != "stock":
|
||||
asset_location = HOLIDAY_THEME_PATH / self.holiday_theme / asset_type
|
||||
elif theme in HOLIDAY_SLUGS:
|
||||
asset_location = HOLIDAY_THEME_PATH / theme / asset_type
|
||||
elif f"{theme}_week" in HOLIDAY_SLUGS:
|
||||
asset_location = HOLIDAY_THEME_PATH / f"{theme}_week" / asset_type
|
||||
else:
|
||||
download_link = f"{GITLAB_URL}/Themes/{theme_name}/{theme_component}"
|
||||
asset_location = THEME_SAVE_PATH / "theme_packs" / theme / asset_type
|
||||
|
||||
if theme_path.is_file():
|
||||
delete_file(theme_path)
|
||||
if not asset_location.exists() or theme == "stock":
|
||||
asset_location = STOCKOP_THEME_PATH / asset_type
|
||||
print(f"Using the stock {asset_type[:-1]} instead")
|
||||
|
||||
theme_url = download_link + ext
|
||||
print(f"Downloading theme from GitLab: {theme_name}")
|
||||
download_file(CANCEL_DOWNLOAD_PARAM, theme_path, DOWNLOAD_PROGRESS_PARAM, theme_url, theme_param, self.session)
|
||||
delete_file(save_location, print_error=not boot_run)
|
||||
|
||||
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
|
||||
handle_error(None, "Download cancelled...", "Download cancelled...", theme_param, DOWNLOAD_PROGRESS_PARAM)
|
||||
self.downloading_theme = False
|
||||
return
|
||||
|
||||
if verify_download(theme_path, theme_url, self.session):
|
||||
print(f"Theme {theme_name} downloaded and verified successfully from GitLab!")
|
||||
if ext == ".zip":
|
||||
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Unpacking theme...")
|
||||
extract_zip(theme_path, download_path)
|
||||
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Downloaded!")
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def download_theme(self, theme_component, theme_name, theme_param):
|
||||
self.downloading_theme = True
|
||||
|
||||
repo_url = get_repository_url(self.session)
|
||||
if not repo_url:
|
||||
handle_error(None, "GitHub and GitLab are offline...", "Repository unavailable", theme_param, DOWNLOAD_PROGRESS_PARAM)
|
||||
self.downloading_theme = False
|
||||
return
|
||||
|
||||
if theme_component == "steering_wheels":
|
||||
download_link = f"{repo_url}/Steering-Wheels/{theme_name}"
|
||||
download_path = THEME_SAVE_PATH / theme_component / theme_name
|
||||
extensions = [".gif", ".png"]
|
||||
else:
|
||||
download_link = f"{repo_url}/Themes/{theme_name}/{theme_component}"
|
||||
download_path = THEME_SAVE_PATH / "theme_packs" / theme_name / theme_component
|
||||
extensions = [".zip"]
|
||||
|
||||
for ext in extensions:
|
||||
theme_path = download_path.with_suffix(ext)
|
||||
if theme_path.is_file():
|
||||
delete_file(theme_path)
|
||||
|
||||
theme_url = download_link + ext
|
||||
print(f"Downloading theme from GitHub: {theme_name}")
|
||||
download_file(CANCEL_DOWNLOAD_PARAM, theme_path, DOWNLOAD_PROGRESS_PARAM, theme_url, theme_param, self.session)
|
||||
|
||||
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
|
||||
handle_error(None, "Download cancelled...", "Download cancelled...", theme_param, DOWNLOAD_PROGRESS_PARAM)
|
||||
self.downloading_theme = False
|
||||
return
|
||||
|
||||
if verify_download(theme_path, theme_url, self.session):
|
||||
print(f"Theme {theme_name} downloaded and verified successfully from GitHub!")
|
||||
if ext == ".zip":
|
||||
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Unpacking theme...")
|
||||
extract_zip(theme_path, download_path)
|
||||
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Downloaded!")
|
||||
self.downloading_theme = False
|
||||
return
|
||||
elif self.handle_verification_failure(ext, theme_component, theme_name, theme_param, theme_path, download_path):
|
||||
self.downloading_theme = False
|
||||
return
|
||||
|
||||
handle_error(download_path, "Download failed...", "Download failed...", theme_param, DOWNLOAD_PROGRESS_PARAM)
|
||||
self.downloading_theme = False
|
||||
|
||||
def fetch_assets(self, repo_url):
|
||||
branches = ["Distance-Icons", "Steering-Wheels", "Themes"]
|
||||
|
||||
assets = {
|
||||
"themes": {},
|
||||
"wheels": []
|
||||
}
|
||||
|
||||
try:
|
||||
for branch in branches:
|
||||
if "github" in repo_url:
|
||||
api_url = f"https://api.github.com/repos/{RESOURCES_REPO}/git/trees/{branch}?recursive=1"
|
||||
elif "gitlab" in repo_url:
|
||||
api_url = f"https://gitlab.com/api/v4/projects/{RESOURCES_REPO.replace('/', '%2F')}/repository/tree?ref={branch}&recursive=true"
|
||||
else:
|
||||
print(f"Unsupported repository URL: {repo_url}")
|
||||
return assets
|
||||
|
||||
print(f"Fetching assets from branch '{branch}': {api_url}")
|
||||
response = self.session.get(api_url, timeout=10)
|
||||
response.raise_for_status()
|
||||
content = response.json()
|
||||
|
||||
if "github" in repo_url:
|
||||
content = content.get("tree", [])
|
||||
|
||||
for item in content:
|
||||
if item["type"] != "blob":
|
||||
continue
|
||||
|
||||
if branch == "Steering-Wheels":
|
||||
assets["wheels"].append(item["path"])
|
||||
elif branch == "Themes":
|
||||
theme_name = item["path"].split("/")[0]
|
||||
assets["themes"].setdefault(theme_name, set())
|
||||
|
||||
item_path = item["path"].lower()
|
||||
if "colors" in item_path:
|
||||
assets["themes"][theme_name].add("colors")
|
||||
elif "distance_icons" in item_path:
|
||||
assets["themes"][theme_name].add("distance_icons")
|
||||
elif "icons" in item_path:
|
||||
assets["themes"][theme_name].add("icons")
|
||||
elif "signals" in item_path:
|
||||
assets["themes"][theme_name].add("signals")
|
||||
elif "sounds" in item_path:
|
||||
assets["themes"][theme_name].add("sounds")
|
||||
|
||||
return {**assets, "themes": {k: list(v) for k, v in assets["themes"].items()}}
|
||||
except requests.exceptions.RequestException as error:
|
||||
handle_request_error(f"Failed to fetch theme sizes from {'GitHub' if 'github' in repo_url else 'GitLab'}: {error}", None, None, None)
|
||||
return {}
|
||||
save_location.parent.mkdir(parents=True, exist_ok=True)
|
||||
save_location.symlink_to(asset_location, target_is_directory=True)
|
||||
print(f"Linked {save_location} to {asset_location}")
|
||||
|
||||
def update_theme_params(self, downloadable_colors, downloadable_distance_icons, downloadable_icons, downloadable_signals, downloadable_sounds, downloadable_wheels):
|
||||
def update_param(key, assets, subfolder):
|
||||
if subfolder == "steering_wheels":
|
||||
themes_path = THEME_SAVE_PATH / subfolder
|
||||
existing_assets = {item.stem.replace("_", " ").title() for item in themes_path.glob("*") if item.is_file()}
|
||||
existing_assets = {self.format_name(item.name, "steering_wheels") for item in themes_path.glob("*") if item.is_file()}
|
||||
else:
|
||||
themes_path = THEME_SAVE_PATH / "theme_packs"
|
||||
existing_assets = {item.parent.name.replace("_", " ").title() for item in themes_path.glob(f"*/{subfolder}") if item.is_dir()}
|
||||
existing_assets = {self.format_name(item.parent.name, subfolder) for item in themes_path.glob(f"*/{subfolder}") if item.is_dir()}
|
||||
|
||||
params.put(key, ",".join(sorted(set(assets) - existing_assets)))
|
||||
print(f"{key} updated successfully")
|
||||
|
||||
@@ -438,56 +518,46 @@ class ThemeManager:
|
||||
update_param("DownloadableSounds", downloadable_sounds, "sounds")
|
||||
update_param("DownloadableWheels", downloadable_wheels, "steering_wheels")
|
||||
|
||||
def validate_themes(self, downloadable_colors, downloadable_distance_icons, downloadable_icons, downloadable_signals, downloadable_sounds, downloadable_wheels, frogpilot_toggles):
|
||||
asset_mappings = {
|
||||
"CustomColors": ("colors", frogpilot_toggles.color_scheme, downloadable_colors),
|
||||
"CustomDistanceIcons": ("distance_icons", frogpilot_toggles.distance_icons, downloadable_distance_icons),
|
||||
"CustomIcons": ("icons", frogpilot_toggles.icon_pack, downloadable_icons),
|
||||
"CustomSounds": ("sounds", frogpilot_toggles.sound_pack, downloadable_sounds),
|
||||
"CustomSignals": ("signals", frogpilot_toggles.signal_icons, downloadable_signals),
|
||||
"WheelIcon": ("steering_wheels", frogpilot_toggles.wheel_image, downloadable_wheels)
|
||||
}
|
||||
downloaded_themes = {}
|
||||
for theme_dir in (THEME_SAVE_PATH / "theme_packs").iterdir():
|
||||
components = []
|
||||
for component in ["colors", "distance_icons", "icons", "signals", "sounds"]:
|
||||
if (theme_dir / component).is_dir():
|
||||
components.append(component)
|
||||
|
||||
for theme_param, (theme_component, theme_name, downloadable_list) in asset_mappings.items():
|
||||
if not downloadable_list:
|
||||
continue
|
||||
if components:
|
||||
theme_name = self.format_name(theme_dir.name, "theme_packs")
|
||||
downloaded_themes[theme_name] = sorted(components)
|
||||
|
||||
if theme_name.lower() in {"none", "stock"}:
|
||||
continue
|
||||
downloaded_wheels = []
|
||||
for wheel_file in (THEME_SAVE_PATH / "steering_wheels").iterdir():
|
||||
if wheel_file.is_file():
|
||||
downloaded_wheels.append(self.format_name(wheel_file.name, "steering_wheels"))
|
||||
|
||||
if theme_component == "steering_wheels":
|
||||
theme_path = THEME_SAVE_PATH / "steering_wheels" / theme_name
|
||||
matching_files = list(theme_path.parent.glob(f"{theme_name}.*"))
|
||||
if not matching_files:
|
||||
print(f" {theme_name} for {theme_component} not found. Downloading...")
|
||||
self.download_theme(theme_component, theme_name, theme_param)
|
||||
update_frogpilot_toggles()
|
||||
elif theme_name.replace("_", " ").split(".")[0].title() not in downloadable_list:
|
||||
if theme_path.exists():
|
||||
print(f"{theme_name} for {theme_component} is outdated. Deleting...")
|
||||
delete_file(theme_path)
|
||||
continue
|
||||
else:
|
||||
theme_path = THEME_SAVE_PATH / "theme_packs" / theme_name / theme_component
|
||||
if not theme_path.exists():
|
||||
print(f" {theme_name} for {theme_component} not found. Downloading...")
|
||||
self.download_theme(theme_component, theme_name, theme_param)
|
||||
update_frogpilot_toggles()
|
||||
elif theme_name.replace("_", " ").split(".")[0].title() not in downloadable_list:
|
||||
if theme_path.exists():
|
||||
print(f"{theme_name} for {theme_component} is outdated. Deleting...")
|
||||
delete_file(theme_path)
|
||||
continue
|
||||
params.put("ThemesDownloaded", json.dumps({
|
||||
"themes": {key: downloaded_themes[key] for key in sorted(downloaded_themes)},
|
||||
"steering_wheels": sorted(downloaded_wheels)
|
||||
}))
|
||||
|
||||
for dir_path in THEME_SAVE_PATH.glob("**/*"):
|
||||
if dir_path.is_dir() and not any(dir_path.iterdir()):
|
||||
print(f"Deleting empty folder: {dir_path}")
|
||||
dir_path.rmdir()
|
||||
elif dir_path.is_file() and dir_path.name.startswith("tmp"):
|
||||
print(f"Deleting temp file: {dir_path}")
|
||||
dir_path.unlink()
|
||||
print("ThemesDownloaded updated successfully")
|
||||
|
||||
print("Theme validation complete.")
|
||||
def update_theme_size(self, theme_component, theme_name, file_size):
|
||||
if theme_component == "steering_wheels":
|
||||
key = "wheels"
|
||||
else:
|
||||
key = "themes"
|
||||
|
||||
if key not in self.theme_sizes:
|
||||
self.theme_sizes[key] = {}
|
||||
|
||||
if key == "wheels":
|
||||
self.theme_sizes[key][theme_name] = file_size
|
||||
else:
|
||||
if theme_name not in self.theme_sizes[key]:
|
||||
self.theme_sizes[key][theme_name] = {}
|
||||
self.theme_sizes[key][theme_name][theme_component] = file_size
|
||||
|
||||
update_json_file(self.theme_sizes_path, self.theme_sizes)
|
||||
|
||||
def update_themes(self, frogpilot_toggles, boot_run=False):
|
||||
if self.downloading_theme:
|
||||
@@ -498,7 +568,7 @@ class ThemeManager:
|
||||
print("GitHub and GitLab are offline...")
|
||||
return
|
||||
|
||||
assets = self.fetch_assets(repo_url)
|
||||
assets = self.fetch_assets(repo_url, frogpilot_toggles)
|
||||
if not assets:
|
||||
return
|
||||
|
||||
@@ -509,7 +579,7 @@ class ThemeManager:
|
||||
downloadable_sounds = []
|
||||
|
||||
for theme, available_assets in assets["themes"].items():
|
||||
theme_name = theme.replace("_", " ").split(".")[0].title()
|
||||
theme_name = self.format_name(theme, "theme_packs")
|
||||
print(f"Theme found: {theme_name}")
|
||||
|
||||
if "colors" in available_assets:
|
||||
@@ -523,7 +593,7 @@ class ThemeManager:
|
||||
if "sounds" in available_assets:
|
||||
downloadable_sounds.append(theme_name)
|
||||
|
||||
downloadable_wheels = [wheel.replace("_", " ").split(".")[0].title() for wheel in assets["wheels"]]
|
||||
downloadable_wheels = [self.format_name(wheel, "steering_wheels") for wheel in assets["wheels"]]
|
||||
|
||||
print(f"Downloadable Colors: {downloadable_colors}")
|
||||
print(f"Downloadable Icons: {downloadable_icons}")
|
||||
@@ -536,3 +606,67 @@ class ThemeManager:
|
||||
self.validate_themes(downloadable_colors, downloadable_distance_icons, downloadable_icons, downloadable_signals, downloadable_sounds, downloadable_wheels, frogpilot_toggles)
|
||||
|
||||
self.update_theme_params(downloadable_colors, downloadable_distance_icons, downloadable_icons, downloadable_signals, downloadable_sounds, downloadable_wheels)
|
||||
|
||||
def update_wheel_image(self, image, boot_run=False, random_event=False):
|
||||
wheel_save_location = ACTIVE_THEME_PATH / "steering_wheel"
|
||||
|
||||
if self.holiday_theme != "stock":
|
||||
wheel_location = HOLIDAY_THEME_PATH / self.holiday_theme / "steering_wheel"
|
||||
elif random_event:
|
||||
wheel_location = RANDOM_EVENTS_PATH / "steering_wheels"
|
||||
elif image == "stock":
|
||||
wheel_location = STOCKOP_THEME_PATH / "steering_wheel"
|
||||
elif image in HOLIDAY_SLUGS:
|
||||
wheel_location = HOLIDAY_THEME_PATH / image / "steering_wheel"
|
||||
elif f"{image}_week" in HOLIDAY_SLUGS:
|
||||
wheel_location = HOLIDAY_THEME_PATH / f"{image}_week" / "steering_wheel"
|
||||
else:
|
||||
wheel_location = THEME_SAVE_PATH / "steering_wheels"
|
||||
|
||||
if not wheel_location.exists():
|
||||
wheel_location = STOCKOP_THEME_PATH / "steering_wheel"
|
||||
print("Using the stock steering wheel instead")
|
||||
|
||||
delete_file(wheel_save_location, print_error=not boot_run)
|
||||
wheel_save_location.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
image_name = image.replace(" ", "_").lower()
|
||||
matching_files = [images for images in wheel_location.iterdir() if images.stem.lower() in {image_name, "wheel"}]
|
||||
if matching_files:
|
||||
source_file = matching_files[0]
|
||||
destination_file = wheel_save_location / f"wheel{source_file.suffix}"
|
||||
destination_file.symlink_to(source_file)
|
||||
print(f"Linked {destination_file} to {source_file}")
|
||||
|
||||
def validate_themes(self, downloadable_colors, downloadable_distance_icons, downloadable_icons, downloadable_signals, downloadable_sounds, downloadable_wheels, frogpilot_toggles):
|
||||
downloaded_data = json.loads(params.get("ThemesDownloaded") or "{}")
|
||||
|
||||
for display_name, components in downloaded_data.get("themes", {}).items():
|
||||
raw_name = display_name.lower().replace(" ", "_").replace("(", "").replace(")", "")
|
||||
theme_folder_name = raw_name.replace("_animated", "-animated")
|
||||
|
||||
for component in components:
|
||||
component_path = THEME_SAVE_PATH / "theme_packs" / theme_folder_name / component
|
||||
if not component_path.is_dir() or not any(component_path.iterdir()):
|
||||
print(f"Missing or empty component '{component}' for theme '{theme_folder_name}'. Downloading...")
|
||||
self.download_theme(component, theme_folder_name, THEME_COMPONENT_PARAMS.get(component), frogpilot_toggles)
|
||||
self.update_active_theme(True, frogpilot_toggles)
|
||||
|
||||
wheels_path = THEME_SAVE_PATH / "steering_wheels"
|
||||
for display_name in downloaded_data.get("steering_wheels", []):
|
||||
file_stem = display_name.replace(" ", "_").lower()
|
||||
matching_files = list(wheels_path.glob(f"{file_stem}.*"))
|
||||
if not matching_files:
|
||||
print(f"Missing steering wheel '{display_name}'. Downloading...")
|
||||
self.download_theme("steering_wheels", file_stem, THEME_COMPONENT_PARAMS["steering_wheels"], frogpilot_toggles)
|
||||
self.update_active_theme(True, frogpilot_toggles)
|
||||
|
||||
for dir_path in THEME_SAVE_PATH.glob("**/*"):
|
||||
if dir_path.is_dir() and not any(dir_path.iterdir()):
|
||||
print(f"Deleting empty folder: {dir_path}")
|
||||
delete_file(dir_path)
|
||||
elif dir_path.is_file() and dir_path.name.startswith("tmp"):
|
||||
print(f"Deleting temp file: {dir_path}")
|
||||
delete_file(dir_path)
|
||||
|
||||
print("Theme validation complete.")
|
||||
|
||||
@@ -172,7 +172,7 @@ def main(demo=False):
|
||||
cloudlog.warning(f"connected extra cam with buffer size: {vipc_client_extra.buffer_len} ({vipc_client_extra.width} x {vipc_client_extra.height})")
|
||||
|
||||
# messaging
|
||||
pm = PubMaster(["modelV2", "cameraOdometry"])
|
||||
pm = PubMaster(["modelV2", "cameraOdometry", "frogpilotModelV2"])
|
||||
sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "navModel", "navInstruction", "carControl", "liveTracks", "liveDelay", "frogpilotPlan"])
|
||||
|
||||
publish_state = PublishState()
|
||||
@@ -316,6 +316,7 @@ def main(demo=False):
|
||||
|
||||
if model_output is not None:
|
||||
modelv2_send = messaging.new_message('modelV2')
|
||||
frogpilot_modelv2_send = messaging.new_message('frogpilotModelV2')
|
||||
posenet_send = messaging.new_message('cameraOdometry')
|
||||
fill_model_msg(modelv2_send, model_output, publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id, frame_drop_ratio,
|
||||
meta_main.timestamp_eof, timestamp_llk, model_execution_time, live_calib_seen, nav_enabled)
|
||||
@@ -327,10 +328,11 @@ def main(demo=False):
|
||||
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob, sm['frogpilotPlan'], frogpilot_toggles)
|
||||
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
|
||||
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
|
||||
modelv2_send.modelV2.meta.turnDirection = DH.turn_direction
|
||||
frogpilot_modelv2_send.frogpilotModelV2.turnDirection = DH.turn_direction
|
||||
|
||||
fill_pose_msg(posenet_send, model_output, meta_main.frame_id, vipc_dropped_frames, meta_main.timestamp_eof, live_calib_seen)
|
||||
pm.send('modelV2', modelv2_send)
|
||||
pm.send('frogpilotModelV2', frogpilot_modelv2_send)
|
||||
pm.send('cameraOdometry', posenet_send)
|
||||
|
||||
last_vipc_frame_id = meta_main.frame_id
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
from pathlib import Path
|
||||
|
||||
import datetime
|
||||
import filecmp
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
@@ -14,6 +13,8 @@ import threading
|
||||
import time
|
||||
import zstandard as zstd
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.time import system_time_valid
|
||||
@@ -21,11 +22,11 @@ from openpilot.system.athena.registration import register
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
|
||||
from openpilot.frogpilot.assets.model_manager import ModelManager
|
||||
from openpilot.frogpilot.assets.theme_manager import HOLIDAY_THEME_PATH, ThemeManager
|
||||
from openpilot.frogpilot.assets.theme_manager import ThemeManager
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import delete_file, run_cmd, use_konik_server
|
||||
from openpilot.frogpilot.common.frogpilot_variables import (
|
||||
ERROR_LOGS_PATH, EXCLUDED_KEYS, HD_LOGS_PATH, KONIK_LOGS_PATH, MODELS_PATH, SCREEN_RECORDINGS_PATH,
|
||||
THEME_SAVE_PATH, FrogPilotVariables, frogpilot_default_params, get_frogpilot_toggles, params
|
||||
THEME_SAVE_PATH, VIDEO_CACHE_PATH, FrogPilotVariables, frogpilot_default_params, get_frogpilot_toggles, params
|
||||
)
|
||||
from openpilot.frogpilot.system.frogpilot_stats import send_stats
|
||||
|
||||
@@ -130,15 +131,31 @@ def backup_toggles(params_cache):
|
||||
def convert_params(params_cache):
|
||||
print("Starting to convert params")
|
||||
|
||||
if Path("/cache/tracking").exists():
|
||||
params_tracking = Params("/cache/tracking")
|
||||
|
||||
frogpilot_stats = json.loads(params.get("FrogPilotStats") or "{}")
|
||||
frogpilot_stats["FrogPilotDrives"] = params_tracking.get_int("FrogPilotDrives")
|
||||
frogpilot_stats["FrogPilotMeters"] = params_tracking.get_float("FrogPilotKilometers") * 1000
|
||||
frogpilot_stats["FrogPilotSeconds"] = params_tracking.get_float("FrogPilotMinutes") * 60
|
||||
|
||||
params.put("FrogPilotStats", json.dumps(frogpilot_stats))
|
||||
|
||||
delete_file("/cache/tracking")
|
||||
|
||||
print("Param conversion completed")
|
||||
|
||||
def frogpilot_boot_functions(build_metadata, params_cache):
|
||||
if params.get_bool("HasAcceptedTerms"):
|
||||
params_cache.clear_all()
|
||||
|
||||
FrogPilotVariables().update(holiday_theme="stock", started=False, boot_run=True)
|
||||
ModelManager().copy_default_model()
|
||||
ThemeManager().update_active_theme(time_validated=system_time_valid(), frogpilot_toggles=get_frogpilot_toggles(), boot_run=True)
|
||||
FrogPilotVariables().update(holiday_theme="stock", started=False)
|
||||
ModelManager(boot_run=True)
|
||||
ThemeManager(boot_run=True).update_active_theme(time_validated=system_time_valid(), frogpilot_toggles=get_frogpilot_toggles(), boot_run=True)
|
||||
|
||||
if VIDEO_CACHE_PATH.exists():
|
||||
for video in VIDEO_CACHE_PATH.glob("*.mp4"):
|
||||
delete_file(video)
|
||||
|
||||
if use_konik_server():
|
||||
if params.get("KonikDongleId", encoding="utf8") != None:
|
||||
@@ -172,26 +189,6 @@ def setup_frogpilot(build_metadata):
|
||||
SCREEN_RECORDINGS_PATH.mkdir(parents=True, exist_ok=True)
|
||||
THEME_SAVE_PATH.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for source_suffix, destination_suffix in [
|
||||
("world_frog_day/colors", "theme_packs/frog/colors"),
|
||||
("world_frog_day/distance_icons", "theme_packs/frog-animated/distance_icons"),
|
||||
("world_frog_day/icons", "theme_packs/frog-animated/icons"),
|
||||
("world_frog_day/signals", "theme_packs/frog/signals"),
|
||||
("world_frog_day/sounds", "theme_packs/frog/sounds"),
|
||||
]:
|
||||
source = Path(HOLIDAY_THEME_PATH) / source_suffix
|
||||
destination = THEME_SAVE_PATH / destination_suffix
|
||||
destination.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copytree(source, destination, dirs_exist_ok=True)
|
||||
|
||||
for source_suffix, destination_suffix in [
|
||||
("world_frog_day/steering_wheel/wheel.png", "steering_wheels/frog.png"),
|
||||
]:
|
||||
source = Path(HOLIDAY_THEME_PATH) / source_suffix
|
||||
destination = THEME_SAVE_PATH / destination_suffix
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(source, destination)
|
||||
|
||||
boot_logo_location = Path("/usr/comma/bg.jpg")
|
||||
frogpilot_boot_logo = Path(__file__).parents[1] / "assets/other_images/frogpilot_boot_logo.png"
|
||||
if not filecmp.cmp(frogpilot_boot_logo, boot_logo_location, shallow=False):
|
||||
|
||||
@@ -5,17 +5,18 @@ import numpy as np
|
||||
import requests
|
||||
import shutil
|
||||
import subprocess
|
||||
import tarfile
|
||||
import threading
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import zipfile
|
||||
|
||||
import openpilot.system.sentry as sentry
|
||||
|
||||
from functools import cache
|
||||
from pathlib import Path
|
||||
|
||||
import openpilot.system.sentry as sentry
|
||||
|
||||
from cereal import log, messaging
|
||||
from opendbc.can.parser import CANParser
|
||||
from openpilot.common.realtime import DT_DMON, DT_HW
|
||||
@@ -37,6 +38,7 @@ locks = {
|
||||
"update_checks": threading.Lock(),
|
||||
"update_maps": threading.Lock(),
|
||||
"update_openpilot": threading.Lock(),
|
||||
"update_tinygrad": threading.Lock()
|
||||
}
|
||||
|
||||
def run_thread_with_lock(name, target, args=(), report=True):
|
||||
@@ -99,20 +101,44 @@ def calculate_lane_width(lane, current_lane, road_edge=None):
|
||||
def calculate_road_curvature(modelData, v_ego):
|
||||
orientation_rate = np.array(modelData.orientationRate.z)
|
||||
velocity = np.array(modelData.velocity.x)
|
||||
timebase = np.array(modelData.orientationRate.t)
|
||||
|
||||
max_pred_lat_acc = max(np.max(orientation_rate * velocity), np.min(orientation_rate * velocity), key=abs)
|
||||
lateral_acceleration = orientation_rate * velocity
|
||||
index = np.argmax(np.abs(lateral_acceleration))
|
||||
predicted_lateral_acc = float(lateral_acceleration[index])
|
||||
time_to_curve = float(timebase[index])
|
||||
|
||||
return float(max_pred_lat_acc / max(v_ego, 1)**2)
|
||||
return predicted_lateral_acc / max(v_ego, 1)**2, max(time_to_curve, 1)
|
||||
|
||||
def delete_file(path, report=True):
|
||||
def clean_model_name(name):
|
||||
return (
|
||||
name.replace("🗺️", "")
|
||||
.replace("📡", "")
|
||||
.replace("👀", "")
|
||||
.replace("(Default)", "")
|
||||
.strip()
|
||||
)
|
||||
|
||||
def delete_file(path, print_error=True, report=True):
|
||||
path = Path(path)
|
||||
if path.is_file() or path.is_symlink():
|
||||
run_cmd(["sudo", "rm", "-f", str(path)], success_message=f"Deleted file: {path}", fail_message=f"Failed to delete file: {path}", report=report)
|
||||
run_cmd(["sudo", "rm", "-f", str(path)], f"Deleted file: {path}", f"Failed to delete file: {path}", report=report)
|
||||
elif path.is_dir():
|
||||
run_cmd(["sudo", "rm", "-rf", str(path)], success_message=f"Deleted directory: {path}", fail_message=f"Failed to delete directory: {path}", report=report)
|
||||
else:
|
||||
run_cmd(["sudo", "rm", "-rf", str(path)], f"Deleted directory: {path}", f"Failed to delete directory: {path}", report=report)
|
||||
elif print_error:
|
||||
print(f"File not found: {path}")
|
||||
|
||||
def extract_tar(tar_file, extract_path):
|
||||
tar_file = Path(tar_file)
|
||||
extract_path = Path(extract_path)
|
||||
print(f"Extracting {tar_file} to {extract_path}")
|
||||
|
||||
with tarfile.open(tar_file, "r:gz") as tar:
|
||||
tar.extractall(path=extract_path)
|
||||
|
||||
tar_file.unlink()
|
||||
print(f"Extraction completed: {tar_file} has been removed")
|
||||
|
||||
def extract_zip(zip_file, extract_path):
|
||||
zip_file = Path(zip_file)
|
||||
extract_path = Path(extract_path)
|
||||
@@ -146,16 +172,22 @@ def is_url_pingable(url):
|
||||
headers = {"User-Agent": "frogpilot-ping-test/1.0 (https://github.com/FrogAi/FrogPilot)"}
|
||||
try:
|
||||
response = requests.head(url, headers=headers, timeout=10, allow_redirects=True)
|
||||
response.raise_for_status()
|
||||
return True
|
||||
except requests.RequestException as exception:
|
||||
print(f"Network/HTTP error for {url}: {exception}")
|
||||
if response.status_code in (405, 501):
|
||||
response = requests.get(url, headers=headers, timeout=10, allow_redirects=True, stream=True)
|
||||
return response.ok
|
||||
except requests.exceptions.RequestException as error:
|
||||
print(f"{error.__class__.__name__} while pinging {url}: {error}")
|
||||
return False
|
||||
except Exception as exception:
|
||||
print(f"An unexpected error occurred while checking {url}: {exception}")
|
||||
sentry.capture_exception(exception)
|
||||
print(f"Unexpected error while pinging {url}: {exception}")
|
||||
return False
|
||||
|
||||
def load_json_file(path):
|
||||
if path.is_file():
|
||||
with open(path) as file:
|
||||
return json.load(file)
|
||||
return {}
|
||||
|
||||
def lock_doors(lock_doors_timer, sm):
|
||||
wait_for_no_driver(sm, door_checks=True, time_threshold=lock_doors_timer)
|
||||
|
||||
@@ -180,19 +212,26 @@ def lock_doors(lock_doors_timer, sm):
|
||||
|
||||
def run_cmd(cmd, success_message, fail_message, report=True, env=None):
|
||||
try:
|
||||
subprocess.run(cmd, capture_output=True, check=True, env=env, text=True)
|
||||
result = subprocess.run(cmd, capture_output=True, check=True, env=env, text=True)
|
||||
print(success_message)
|
||||
return result.stdout.strip()
|
||||
except subprocess.CalledProcessError as error:
|
||||
print(f"Command failed with return code {error.returncode}")
|
||||
if error.stderr:
|
||||
print(f"Error Output: {error.stderr.strip()}")
|
||||
if report:
|
||||
sentry.capture_exception(error)
|
||||
return None
|
||||
except Exception as exception:
|
||||
print(f"Unexpected error occurred: {exception}")
|
||||
print(fail_message)
|
||||
if report:
|
||||
sentry.capture_exception(exception)
|
||||
return None
|
||||
|
||||
def update_json_file(path, data):
|
||||
with open(path, "w") as file:
|
||||
json.dump(data, file, indent=2, sort_keys=True)
|
||||
|
||||
def update_maps(now):
|
||||
while not MAPD_PATH.exists():
|
||||
@@ -233,7 +272,7 @@ def update_maps(now):
|
||||
|
||||
def update_openpilot():
|
||||
def update_available():
|
||||
subprocess.run(["pkill", "-SIGUSR1", "-f", "system.updated.updated"], check=False)
|
||||
run_cmd(["pkill", "-SIGUSR1", "-f", "system.updated.updated"], "Updater check signal sent", "Failed to send updater check signal", report=False)
|
||||
|
||||
while params.get("UpdaterState", encoding="utf-8") != "checking...":
|
||||
time.sleep(1)
|
||||
@@ -247,25 +286,22 @@ def update_openpilot():
|
||||
while params.get("UpdaterState", encoding="utf-8") != "idle":
|
||||
time.sleep(60)
|
||||
|
||||
subprocess.run(["pkill", "-SIGHUP", "-f", "system.updated.updated"], check=False)
|
||||
run_cmd(["pkill", "-SIGHUP", "-f", "system.updated.updated"], "Updater refresh signal sent", "Failed to send updater refresh signal", report=False)
|
||||
|
||||
while not params.get_bool("UpdateAvailable"):
|
||||
time.sleep(60)
|
||||
|
||||
return True
|
||||
|
||||
while params_memory.get_bool("DownloadAllModels") or params_memory.get("ModelToDownload") or params_memory.get_bool("UpdateSpeedLimits"):
|
||||
time.sleep(60)
|
||||
|
||||
if params.get("UpdaterState", encoding="utf-8") != "idle":
|
||||
return
|
||||
|
||||
while params.get_bool("IsOnroad") or params_memory.get_bool("UpdateSpeedLimits") or running_threads.get("lock_doors", threading.Thread()).is_alive():
|
||||
time.sleep(60)
|
||||
|
||||
if not update_available():
|
||||
return
|
||||
|
||||
while running_threads.get("lock_doors", threading.Thread()).is_alive() or params_memory.get_bool("IsOnroad"):
|
||||
time.sleep(60)
|
||||
|
||||
while True:
|
||||
if not update_available():
|
||||
break
|
||||
|
||||
@@ -23,20 +23,21 @@ from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.system.hardware.power_monitoring import VBATT_PAUSE_CHARGING
|
||||
from openpilot.system.version import get_build_metadata
|
||||
from panda import ALTERNATIVE_EXPERIENCE, Panda
|
||||
from panda import ALTERNATIVE_EXPERIENCE
|
||||
|
||||
params = Params()
|
||||
params_cache = Params("/cache/params")
|
||||
params_default = Params("/dev/shm/params_default")
|
||||
params_memory = Params("/dev/shm/params")
|
||||
params_tracking = Params("/cache/tracking")
|
||||
|
||||
GearShifter = car.CarState.GearShifter
|
||||
SafetyModel = car.CarParams.SafetyModel
|
||||
|
||||
CITY_SPEED_LIMIT = 25 # 55mph is typically the minimum speed for highways
|
||||
CRUISING_SPEED = 5 # Roughly the speed cars go when not touching the gas while in drive
|
||||
DEFAULT_LATERAL_ACCELERATION = 2.0 # m/s^2, typical lateral acceleration when taking curves
|
||||
EARTH_RADIUS = 6378137 # Radius of the Earth in meters
|
||||
MAX_T_FOLLOW = 3.0 # Maximum allowed following duration. Larger values risk losing track of the lead but may be increased as models improve
|
||||
MINIMUM_LATERAL_ACCELERATION = 1.3 # m/s^2, typical minimum lateral acceleration when taking curves
|
||||
PLANNER_TIME = ModelConstants.T_IDXS[-1] # Length of time the model projects out for
|
||||
THRESHOLD = 0.63 # Requires the condition to be true for ~1 second
|
||||
@@ -53,6 +54,7 @@ THEME_SAVE_PATH = Path("/data/themes")
|
||||
|
||||
ERROR_LOGS_PATH = Path("/data/error_logs")
|
||||
SCREEN_RECORDINGS_PATH = Path("/data/media/screen_recordings")
|
||||
VIDEO_CACHE_PATH = Path("/data/video_cache")
|
||||
|
||||
BACKUP_PATH = Path("/cache/on_backup")
|
||||
|
||||
@@ -65,20 +67,11 @@ KONIK_PATH = Path("/cache/use_konik")
|
||||
MAPD_PATH = Path("/data/media/0/osm/mapd")
|
||||
MAPS_PATH = Path("/data/media/0/osm/offline")
|
||||
|
||||
NEURAL_PARAMS_PATH = Path(BASEDIR) / "selfdrive/car/torque_data/neural_ff_weights.json"
|
||||
TORQUE_NN_MODEL_PATH = Path(BASEDIR) / "frogpilot/assets/nnff_models"
|
||||
NNFF_MODELS_PATH = Path(BASEDIR) / "frogpilot/assets/nnff_models"
|
||||
|
||||
DEFAULT_CLASSIC_MODEL = "wd-40"
|
||||
DEFAULT_CLASSIC_MODEL_NAME = "WD-40 (Default) 👀📡"
|
||||
DEFAULT_CLASSIC_MODEL_VERSION = "v2"
|
||||
|
||||
DEFAULT_MODEL = "national-public-radio"
|
||||
DEFAULT_MODEL_NAME = "National Public Radio 👀📡"
|
||||
DEFAULT_MODEL_VERSION = "v6"
|
||||
|
||||
DEFAULT_TINYGRAD_MODEL = "space-lab"
|
||||
DEFAULT_TINYGRAD_MODEL_NAME = "Space Lab 👀📡"
|
||||
DEFAULT_TINYGRAD_MODEL_VERSION = "v7"
|
||||
DEFAULT_MODEL = "firehose"
|
||||
DEFAULT_MODEL_NAME = "Firehose (Default) 👀📡"
|
||||
DEFAULT_MODEL_VERSION = "v9"
|
||||
|
||||
BUTTON_FUNCTIONS = {
|
||||
"NOTHING": 0,
|
||||
@@ -91,9 +84,9 @@ BUTTON_FUNCTIONS = {
|
||||
}
|
||||
|
||||
EXCLUDED_KEYS = {
|
||||
"AvailableModels", "AvailableModelNames", "CarParamsPersistent",
|
||||
"ExperimentalLongitudinalEnabled", "KonikMinutes", "MapBoxRequests", "ModelDrivesAndScores",
|
||||
"ModelVersions", "openpilotMinutes", "OverpassRequests", "SpeedLimits", "SpeedLimitsFiltered", "UpdaterAvailableBranches"
|
||||
"AvailableModels", "AvailableModelNames", "CalibratedLateralAcceleration", "CalibrationProgress", "CarParamsPersistent",
|
||||
"CurvatureData", "ExperimentalLongitudinalEnabled", "KonikMinutes", "MapBoxRequests", "ModelDrivesAndScores", "ModelVersions",
|
||||
"openpilotMinutes", "OverpassRequests", "SpeedLimits", "SpeedLimitsFiltered", "UpdaterAvailableBranches"
|
||||
}
|
||||
|
||||
TINYGRAD_FILES = [
|
||||
@@ -103,17 +96,9 @@ TINYGRAD_FILES = [
|
||||
("driving_vision_tinygrad.pkl", "vision model"),
|
||||
]
|
||||
|
||||
@cache
|
||||
def get_comma_nnff_model_file():
|
||||
with open(NEURAL_PARAMS_PATH, "r") as file:
|
||||
return json.load(file)
|
||||
|
||||
def comma_nnff_supported(car):
|
||||
return car in get_comma_nnff_model_file()
|
||||
|
||||
@cache
|
||||
def get_nnff_model_files():
|
||||
model_dir = Path(TORQUE_NN_MODEL_PATH)
|
||||
model_dir = Path(NNFF_MODELS_PATH)
|
||||
return [file.stem for file in model_dir.iterdir() if file.is_file()]
|
||||
|
||||
def nnff_supported(car_fingerprint):
|
||||
@@ -123,8 +108,8 @@ def nnff_supported(car_fingerprint):
|
||||
|
||||
return False
|
||||
|
||||
def get_frogpilot_toggles(block=True):
|
||||
return SimpleNamespace(**json.loads(params_memory.get("FrogPilotToggles", block=block) or "{}"))
|
||||
def get_frogpilot_toggles():
|
||||
return SimpleNamespace(**json.loads(params_memory.get("FrogPilotToggles") or "{}"))
|
||||
|
||||
def update_frogpilot_toggles():
|
||||
params_memory.put_bool("FrogPilotTogglesUpdated", True)
|
||||
@@ -132,11 +117,11 @@ def update_frogpilot_toggles():
|
||||
frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("AccelerationPath", "1", 2, "0"),
|
||||
("AccelerationProfile", "2", 0, "0"),
|
||||
("AdjacentLeadsUI", "0", 3, "0"),
|
||||
("AdjacentLeadsUI", "1", 3, "0"),
|
||||
("AdjacentPath", "0", 3, "0"),
|
||||
("AdjacentPathMetrics", "0", 3, "0"),
|
||||
("AdvancedCustomUI", "0", 2, "0"),
|
||||
("AdvancedLateralTune", "0", 2, "0"),
|
||||
("AdvancedLateralTune", "0", 3, "0"),
|
||||
("AdvancedLongitudinalTune", "0", 3, "0"),
|
||||
("AggressiveFollow", "1.25", 2, "1.25"),
|
||||
("AggressiveJerkAcceleration", "50", 3, "50"),
|
||||
@@ -148,8 +133,8 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("AlertVolumeControl", "0", 2, "0"),
|
||||
("AlwaysOnDM", "0", 0, "0"),
|
||||
("AlwaysOnLateral", "1", 0, "0"),
|
||||
("AlwaysOnLateralLKAS", "1", 0, "0"),
|
||||
("AlwaysOnLateralMain", "1", 0, "0"),
|
||||
("AlwaysOnLateralLKAS", "1", 2, "0"),
|
||||
("AlwaysOnLateralMain", "1", 2, "0"),
|
||||
("AMapKey1", "", 0, ""),
|
||||
("AMapKey2", "", 0, ""),
|
||||
("AutomaticallyDownloadModels", "1", 1, "0"),
|
||||
@@ -161,6 +146,8 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("BlindSpotMetrics", "1", 3, "0"),
|
||||
("BlindSpotPath", "1", 1, "0"),
|
||||
("BorderMetrics", "0", 3, "0"),
|
||||
("CalibratedLateralAcceleration", str(DEFAULT_LATERAL_ACCELERATION), 2, str(DEFAULT_LATERAL_ACCELERATION)),
|
||||
("CalibrationProgress", "0", 3, "0"),
|
||||
("CameraView", "3", 2, "0"),
|
||||
("CarMake", "", 0, ""),
|
||||
("CarModel", "", 0, ""),
|
||||
@@ -171,7 +158,7 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("CELead", "0", 1, "0"),
|
||||
("CEModelStopTime", str(PLANNER_TIME - 2), 2, "0"),
|
||||
("CENavigation", "1", 2, "0"),
|
||||
("CENavigationIntersections", "1", 2, "0"),
|
||||
("CENavigationIntersections", "0", 2, "0"),
|
||||
("CENavigationLead", "1", 2, "0"),
|
||||
("CENavigationTurns", "1", 2, "0"),
|
||||
("CESignalSpeed", "55", 2, "0"),
|
||||
@@ -182,10 +169,10 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("CEStoppedLead", "0", 1, "0"),
|
||||
("ClusterOffset", "1.015", 2, "1.015"),
|
||||
("Compass", "0", 1, "0"),
|
||||
("ConditionalExperimental", "1", 0, "0"),
|
||||
("CurveSensitivity", "100", 2, "100"),
|
||||
("CurveSpeedControl", "1", 1, "0"),
|
||||
("CustomAlerts", "1", 0, "0"),
|
||||
("ConditionalExperimental", "1", 1, "0"),
|
||||
("CurvatureData", "", 2, ""),
|
||||
("CurveSpeedController", "1", 1, "0"),
|
||||
("CustomAlerts", "0", 0, "0"),
|
||||
("CustomColors", "frog", 0, "stock"),
|
||||
("CustomCruise", "1", 2, "1"),
|
||||
("CustomCruiseLong", "5", 2, "5"),
|
||||
@@ -216,19 +203,19 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("DistanceButtonControl", "1", 2, "0"),
|
||||
("DriverCamera", "0", 1, "0"),
|
||||
("DynamicPathWidth", "0", 2, "0"),
|
||||
("DynamicPedalsOnUI", "1", 2, "0"),
|
||||
("DynamicPedalsOnUI", "1", 1, "0"),
|
||||
("EngageVolume", "101", 2, "101"),
|
||||
("ExperimentalGMTune", "0", 2, "0"),
|
||||
("ExperimentalLongitudinalEnabled", "0", 0, "0"),
|
||||
("ExperimentalModeConfirmed", "0", 0, "0"),
|
||||
("Fahrenheit", "0", 3, "0"),
|
||||
("FavoriteDestinations", "", 0, ""),
|
||||
("ForceAutoTune", "0", 2, "0"),
|
||||
("ForceAutoTuneOff", "0", 2, "0"),
|
||||
("ForceAutoTune", "0", 3, "0"),
|
||||
("ForceAutoTuneOff", "0", 3, "0"),
|
||||
("ForceFingerprint", "0", 2, "0"),
|
||||
("ForceMPHDashboard", "0", 2, "0"),
|
||||
("ForceStandstill", "0", 2, "0"),
|
||||
("ForceMPHDashboard", "0", 3, "0"),
|
||||
("ForceStops", "0", 2, "0"),
|
||||
("ForceTorqueController", "0", 3, "0"),
|
||||
("FPSCounter", "1", 3, "0"),
|
||||
("FrogPilotDongleId", "", 0, ""),
|
||||
("FrogPilotStats", "", 0, ""),
|
||||
@@ -243,11 +230,12 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("GsmRoaming", "1", 0, "0"),
|
||||
("HideAlerts", "0", 2, "0"),
|
||||
("HideLeadMarker", "0", 2, "0"),
|
||||
("HideMap", "0", 2, "0"),
|
||||
("HideMapIcon", "0", 2, "0"),
|
||||
("HideMaxSpeed", "0", 2, "0"),
|
||||
("HideSpeed", "0", 2, "0"),
|
||||
("HideSpeedLimit", "0", 2, "0"),
|
||||
("HigherBitrate", "0", 3, "0"),
|
||||
("HigherBitrate", "0", 2, "0"),
|
||||
("HolidayThemes", "1", 0, "0"),
|
||||
("HumanAcceleration", "1", 2, "0"),
|
||||
("HumanFollowing", "1", 2, "0"),
|
||||
@@ -257,14 +245,14 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("IsMetric", "0", 0, "0"),
|
||||
("KonikDongleId", "", 0, ""),
|
||||
("KonikMinutes", "0", 0, "0"),
|
||||
("LaneChangeCustomizations", "1", 0, "1"),
|
||||
("LaneChangeTime", "2.0", 0, "0"),
|
||||
("LaneDetectionWidth", "0", 2, "0"),
|
||||
("LaneChanges", "1", 0, "1"),
|
||||
("LaneChangeTime", "1.0", 1, "0"),
|
||||
("LaneDetectionWidth", "0", 1, "0"),
|
||||
("LaneLinesWidth", "4", 2, "2"),
|
||||
("LateralTune", "1", 2, "0"),
|
||||
("LateralTune", "1", 1, "0"),
|
||||
("LeadDepartingAlert", "0", 0, "0"),
|
||||
("LeadDetectionThreshold", "35", 3, "50"),
|
||||
("LeadInfo", "1", 2, "0"),
|
||||
("LeadInfo", "1", 3, "0"),
|
||||
("LiveDelay", "", 0, ""),
|
||||
("LKASButtonControl", "5", 2, "0"),
|
||||
("LockDoors", "1", 0, "0"),
|
||||
@@ -275,33 +263,31 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("LongitudinalTune", "1", 0, "0"),
|
||||
("LongPitch", "1", 2, "0"),
|
||||
("LoudBlindspotAlert", "0", 0, "0"),
|
||||
("LowVoltageShutdown", str(VBATT_PAUSE_CHARGING), 2, str(VBATT_PAUSE_CHARGING)),
|
||||
("LowVoltageShutdown", str(VBATT_PAUSE_CHARGING), 3, str(VBATT_PAUSE_CHARGING)),
|
||||
("MapAcceleration", "0", 1, "0"),
|
||||
("MapboxPublicKey", "", 0, ""),
|
||||
("MapboxSecretKey", "", 0, ""),
|
||||
("MapDeceleration", "0", 1, "0"),
|
||||
("MapGears", "0", 1, "0"),
|
||||
("MapGears", "0", 2, "0"),
|
||||
("MapsSelected", "", 0, ""),
|
||||
("MapStyle", "1", 2, "0"),
|
||||
("MapTurnControl", "1", 1, "0"),
|
||||
("MaxDesiredAcceleration", "4.0", 3, "2.0"),
|
||||
("MaxDesiredAcceleration", "4.0", 2, "2.0"),
|
||||
("MinimumLaneChangeSpeed", str(LANE_CHANGE_SPEED_MIN / CV.MPH_TO_MS), 2, str(LANE_CHANGE_SPEED_MIN / CV.MPH_TO_MS)),
|
||||
("Model", DEFAULT_CLASSIC_MODEL, 1, DEFAULT_CLASSIC_MODEL),
|
||||
("Model", DEFAULT_MODEL + "_default", 1, DEFAULT_MODEL + "_default"),
|
||||
("ModelDrivesAndScores", "", 2, ""),
|
||||
("ModelRandomizer", "0", 2, "0"),
|
||||
("ModelUI", "1", 2, "0"),
|
||||
("ModelVersions", "", 2, ""),
|
||||
("MTSCCurvatureCheck", "1", 2, "1"),
|
||||
("NavigationUI", "1", 1, "0"),
|
||||
("NavSettingLeftSide", "0", 0, "0"),
|
||||
("NavSettingTime24h", "0", 0, "0"),
|
||||
("NewLongAPI", "1", 2, "1"),
|
||||
("NewLongAPI", "1", 3, "1"),
|
||||
("NNFF", "1", 2, "0"),
|
||||
("NNFFLite", "1", 2, "0"),
|
||||
("NoLogging", "0", 2, "0"),
|
||||
("NoUploads", "0", 2, "0"),
|
||||
("NudgelessLaneChange", "0", 0, "0"),
|
||||
("NumericalTemp", "1", 2, "0"),
|
||||
("NudgelessLaneChange", "1", 0, "0"),
|
||||
("NumericalTemp", "1", 3, "0"),
|
||||
("Offset1", "5", 0, "0"),
|
||||
("Offset2", "5", 0, "0"),
|
||||
("Offset3", "5", 0, "0"),
|
||||
@@ -314,15 +300,15 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("openpilotMinutes", "0", 0, "0"),
|
||||
("PathEdgeWidth", "20", 2, "0"),
|
||||
("PathWidth", "6.1", 2, "5.9"),
|
||||
("PauseAOLOnBrake", "0", 2, "0"),
|
||||
("PauseLateralOnSignal", "0", 2, "0"),
|
||||
("PauseLateralSpeed", "0", 2, "0"),
|
||||
("PedalsOnUI", "0", 2, "0"),
|
||||
("PauseAOLOnBrake", "0", 1, "0"),
|
||||
("PauseLateralOnSignal", "0", 1, "0"),
|
||||
("PauseLateralSpeed", "0", 1, "0"),
|
||||
("PedalsOnUI", "0", 1, "0"),
|
||||
("PersonalizeOpenpilot", "1", 0, "0"),
|
||||
("PreferredSchedule", "2", 0, "0"),
|
||||
("PromptDistractedVolume", "101", 2, "101"),
|
||||
("PromptVolume", "101", 2, "101"),
|
||||
("QOLLateral", "1", 2, "0"),
|
||||
("QOLLateral", "1", 1, "0"),
|
||||
("QOLLongitudinal", "1", 1, "0"),
|
||||
("QOLVisuals", "1", 0, "0"),
|
||||
("RadarTracksUI", "0", 3, "0"),
|
||||
@@ -340,11 +326,11 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("RelaxedPersonalityProfile", "1", 2, "0"),
|
||||
("ReverseCruise", "0", 1, "0"),
|
||||
("RoadEdgesWidth", "2", 2, "2"),
|
||||
("RoadNameUI", "1", 2, "0"),
|
||||
("RoadNameUI", "1", 1, "0"),
|
||||
("RotatingWheel", "1", 1, "0"),
|
||||
("ScreenBrightness", "101", 2, "101"),
|
||||
("ScreenBrightnessOnroad", "101", 2, "101"),
|
||||
("ScreenManagement", "1", 2, "0"),
|
||||
("ScreenManagement", "1", 1, "0"),
|
||||
("ScreenRecorder", "1", 2, "0"),
|
||||
("ScreenTimeout", "30", 2, "30"),
|
||||
("ScreenTimeoutOnroad", "30", 2, "10"),
|
||||
@@ -359,15 +345,16 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("ShowGPU", "0", 3, "0"),
|
||||
("ShowIP", "0", 3, "0"),
|
||||
("ShowMemoryUsage", "1", 3, "0"),
|
||||
("ShownToggleDescriptions", "", 0, ""),
|
||||
("ShowSLCOffset", "1", 0, "0"),
|
||||
("ShowSpeedLimits", "1", 1, "0"),
|
||||
("ShowSteering", "0", 3, "0"),
|
||||
("ShowStoppingPoint", "0", 2, "0"),
|
||||
("ShowStoppingPointMetrics", "0", 2, "0"),
|
||||
("ShowStoppingPoint", "1", 3, "0"),
|
||||
("ShowStoppingPointMetrics", "1", 3, "0"),
|
||||
("ShowStorageLeft", "0", 3, "0"),
|
||||
("ShowStorageUsed", "0", 3, "0"),
|
||||
("Sidebar", "0", 0, "0"),
|
||||
("SignalMetrics", "0", 2, "0"),
|
||||
("SignalMetrics", "0", 3, "0"),
|
||||
("SLCConfirmation", "0", 0, "0"),
|
||||
("SLCConfirmationHigher", "0", 0, "0"),
|
||||
("SLCConfirmationLower", "0", 0, "0"),
|
||||
@@ -380,10 +367,10 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("SLCPriority2", "Map Data", 2, "Map Data"),
|
||||
("SLCPriority3", "Dashboard", 2, "Dashboard"),
|
||||
("SNGHack", "1", 2, "0"),
|
||||
("SpeedLimitChangedAlert", "1", 0, "0"),
|
||||
("SpeedLimitChangedAlert", "0", 0, "0"),
|
||||
("SpeedLimitController", "1", 0, "0"),
|
||||
("SpeedLimitFiller", "0", 0, "0"),
|
||||
("SpeedLimitSources", "0", 2, "0"),
|
||||
("SpeedLimitSources", "0", 3, "0"),
|
||||
("SshEnabled", "0", 0, "0"),
|
||||
("StartupMessageBottom", "Human-tested, frog-approved 🐸", 0, "Always keep hands on wheel and eyes on road"),
|
||||
("StartupMessageTop", "Hop in and buckle up!", 0, "Be ready to take over at any time"),
|
||||
@@ -394,10 +381,10 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("StandardJerkSpeed", "100", 3, "100"),
|
||||
("StandardJerkSpeedDecrease", "100", 3, "100"),
|
||||
("StandardPersonalityProfile", "1", 2, "0"),
|
||||
("StandbyMode", "0", 2, "0"),
|
||||
("StandbyMode", "0", 1, "0"),
|
||||
("StartAccel", "", 3, ""),
|
||||
("StartAccelStock", "", 3, ""),
|
||||
("StaticPedalsOnUI", "0", 2, "0"),
|
||||
("StaticPedalsOnUI", "0", 1, "0"),
|
||||
("SteerDelay", "", 3, ""),
|
||||
("SteerDelayStock", "", 3, ""),
|
||||
("SteerFriction", "", 3, ""),
|
||||
@@ -416,6 +403,8 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("TacoTune", "0", 2, "0"),
|
||||
("TacoTuneHacks", "0", 2, "0"),
|
||||
("TetheringEnabled", "0", 0, "0"),
|
||||
("ThemesDownloaded", "", 0, ""),
|
||||
("TinygradUpdateAvailable", "0", 1, "0"),
|
||||
("ToyotaDoors", "1", 0, "0"),
|
||||
("TrafficFollow", "0.5", 2, "0.5"),
|
||||
("TrafficJerkAcceleration", "50", 3, "50"),
|
||||
@@ -426,7 +415,6 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("TrafficPersonalityProfile", "1", 2, "0"),
|
||||
("TuningLevel", "0", 0, "0"),
|
||||
("TuningLevelConfirmed", "0", 0, "0"),
|
||||
("TurnAggressiveness", "100", 2, "100"),
|
||||
("TurnDesires", "0", 2, "0"),
|
||||
("UnlimitedLength", "1", 2, "0"),
|
||||
("UnlockDoors", "1", 0, "0"),
|
||||
@@ -439,7 +427,6 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("VEgoStopping", "", 3, ""),
|
||||
("VEgoStoppingStock", "", 3, ""),
|
||||
("VeryLongDistanceButtonControl", "6", 2, "0"),
|
||||
("VisionTurnControl", "1", 1, "0"),
|
||||
("VoltSNG", "0", 2, "0"),
|
||||
("WarningImmediateVolume", "101", 2, "101"),
|
||||
("WarningSoftVolume", "101", 2, "101"),
|
||||
@@ -455,7 +442,7 @@ misc_tuning_levels: list[tuple[str, str | bytes, int, str]] = [
|
||||
|
||||
class FrogPilotVariables:
|
||||
def __init__(self):
|
||||
self.frogpilot_toggles = get_frogpilot_toggles(block=False)
|
||||
self.frogpilot_toggles = get_frogpilot_toggles()
|
||||
self.tuning_levels = {key: lvl for key, _, lvl, _ in frogpilot_default_params + misc_tuning_levels}
|
||||
|
||||
short_branch = get_build_metadata().channel
|
||||
@@ -510,7 +497,7 @@ class FrogPilotVariables:
|
||||
|
||||
params_memory.put("FrogPilotTuningLevels", json.dumps(self.tuning_levels))
|
||||
|
||||
def update(self, holiday_theme, started, boot_run=False):
|
||||
def update(self, holiday_theme, started):
|
||||
default = params_default
|
||||
level = self.tuning_levels
|
||||
toggle = self.frogpilot_toggles
|
||||
@@ -532,7 +519,7 @@ class FrogPilotVariables:
|
||||
CP = cp_reader.as_builder()
|
||||
else:
|
||||
CarInterface, _, _ = interfaces[MOCK.MOCK]
|
||||
CP = CarInterface.get_params(MOCK.MOCK, gen_empty_fingerprint(), [], False, toggle, params, False)
|
||||
CP = CarInterface.get_params(MOCK.MOCK, gen_empty_fingerprint(), [], False, toggle, False)
|
||||
CarInterface.configure_torque_tune(MOCK.MOCK, CP.lateralTuning)
|
||||
|
||||
safety_config = car.CarParams.SafetyConfig.new_message()
|
||||
@@ -545,36 +532,35 @@ class FrogPilotVariables:
|
||||
FPCP = fpcp_reader.as_builder()
|
||||
else:
|
||||
CarInterface, _, _ = interfaces[MOCK.MOCK]
|
||||
FPCP = CarInterface.get_frogpilot_params(MOCK.MOCK, gen_empty_fingerprint(), [], toggle)
|
||||
FPCP = CarInterface.get_frogpilot_params(MOCK.MOCK, gen_empty_fingerprint(), [], CP, toggle)
|
||||
|
||||
is_torque_car = CP.lateralTuning.which() == "torque"
|
||||
is_torque_car = FPCP.lateralTuning.which() == "torque"
|
||||
if not is_torque_car:
|
||||
CarInterfaceBase.configure_torque_tune("MOCK", CP.lateralTuning)
|
||||
CarInterfaceBase.configure_torque_tune(MOCK.MOCK, FPCP.lateralTuning)
|
||||
|
||||
always_on_lateral_set = bool(CP.alternativeExperience & ALTERNATIVE_EXPERIENCE.ALWAYS_ON_LATERAL)
|
||||
toggle.always_on_lateral_set = bool(CP.alternativeExperience & ALTERNATIVE_EXPERIENCE.ALWAYS_ON_LATERAL)
|
||||
toggle.car_make = CP.carName
|
||||
toggle.car_model = CP.carFingerprint
|
||||
toggle.disable_openpilot_long = params.get_bool("DisableOpenpilotLongitudinal") if tuning_level >= level["DisableOpenpilotLongitudinal"] else default.get_bool("DisableOpenpilotLongitudinal")
|
||||
friction = CP.lateralTuning.torque.friction
|
||||
has_auto_tune = toggle.car_make in {"hyundai", "toyota"} and CP.lateralTuning.which() == "torque"
|
||||
friction = FPCP.lateralTuning.torque.friction
|
||||
has_auto_tune = toggle.car_make in {"hyundai", "toyota"} and FPCP.lateralTuning.which() == "torque"
|
||||
has_bsm = CP.enableBsm
|
||||
toggle.has_cc_long = toggle.car_make == "gm" and bool(CP.flags & GMFlags.CC_LONG.value)
|
||||
has_nnff = not comma_nnff_supported(toggle.car_model) and nnff_supported(toggle.car_model)
|
||||
has_nnff = nnff_supported(toggle.car_model)
|
||||
toggle.has_pedal = CP.enableGasInterceptor
|
||||
has_radar = not CP.radarUnavailable
|
||||
toggle.has_sdsu = toggle.car_make == "toyota" and bool(CP.flags & ToyotaFlags.SMART_DSU.value)
|
||||
has_sng = CP.autoResumeSng
|
||||
toggle.has_zss = toggle.car_make == "toyota" and bool(FPCP.fpFlags & ToyotaFrogPilotFlags.ZSS.value)
|
||||
is_angle_car = CP.steerControlType == car.CarParams.SteerControlType.angle
|
||||
latAccelFactor = CP.lateralTuning.torque.latAccelFactor
|
||||
latAccelFactor = FPCP.lateralTuning.torque.latAccelFactor
|
||||
longitudinalActuatorDelay = CP.longitudinalActuatorDelay
|
||||
max_acceleration_enabled = bool(CP.alternativeExperience & ALTERNATIVE_EXPERIENCE.RAISE_LONGITUDINAL_LIMITS_TO_ISO_MAX)
|
||||
toggle.openpilot_longitudinal = CP.openpilotLongitudinalControl and not toggle.disable_openpilot_long
|
||||
pcm_cruise = CP.pcmCruise
|
||||
startAccel = CP.startAccel
|
||||
stopAccel = CP.stopAccel
|
||||
steerActuatorDelay = CP.steerActuatorDelay
|
||||
steerKp = CP.lateralTuning.torque.kp
|
||||
steerKp = FPCP.lateralTuning.torque.kp
|
||||
steerRatio = CP.steerRatio
|
||||
toggle.stoppingDecelRate = CP.stoppingDecelRate
|
||||
taco_hacks_allowed = CP.safetyConfigs[0].safetyModel == SafetyModel.hyundaiCanfd
|
||||
@@ -595,7 +581,8 @@ class FrogPilotVariables:
|
||||
advanced_custom_ui = params.get_bool("AdvancedCustomUI") if tuning_level >= level["AdvancedCustomUI"] else default.get_bool("AdvancedCustomUI")
|
||||
toggle.hide_alerts = advanced_custom_ui and (params.get_bool("HideAlerts") if tuning_level >= level["HideAlerts"] else default.get_bool("HideAlerts")) and not toggle.debug_mode
|
||||
toggle.hide_lead_marker = toggle.openpilot_longitudinal and (advanced_custom_ui and (params.get_bool("HideLeadMarker") if tuning_level >= level["HideLeadMarker"] else default.get_bool("HideLeadMarker")) and not toggle.debug_mode)
|
||||
toggle.hide_map_icon = advanced_custom_ui and (params.get_bool("HideMapIcon") if tuning_level >= level["HideMapIcon"] else default.get_bool("HideMapIcon")) and not toggle.debug_mode
|
||||
toggle.hide_map_icon = advanced_custom_ui and (params.get_bool("HideMapIcon") if tuning_level >= level["HideMapIcon"] else default.get_bool("HideMapIcon"))
|
||||
toggle.hide_map = toggle.hide_map_icon and (params.get_bool("HideMap") if tuning_level >= level["HideMap"] else default.get_bool("HideMap"))
|
||||
toggle.hide_max_speed = advanced_custom_ui and (params.get_bool("HideMaxSpeed") if tuning_level >= level["HideMaxSpeed"] else default.get_bool("HideMaxSpeed")) and not toggle.debug_mode
|
||||
toggle.hide_speed = advanced_custom_ui and (params.get_bool("HideSpeed") if tuning_level >= level["HideSpeed"] else default.get_bool("HideSpeed")) and not toggle.debug_mode
|
||||
toggle.hide_speed_limit = advanced_custom_ui and (params.get_bool("HideSpeedLimit") if tuning_level >= level["HideSpeedLimit"] else default.get_bool("HideSpeedLimit")) and not toggle.debug_mode
|
||||
@@ -605,14 +592,14 @@ class FrogPilotVariables:
|
||||
toggle.force_auto_tune = advanced_lateral_tuning and not has_auto_tune and is_torque_car and (params.get_bool("ForceAutoTune") if tuning_level >= level["ForceAutoTune"] else default.get_bool("ForceAutoTune"))
|
||||
toggle.force_auto_tune_off = advanced_lateral_tuning and has_auto_tune and is_torque_car and (params.get_bool("ForceAutoTuneOff") if tuning_level >= level["ForceAutoTuneOff"] else default.get_bool("ForceAutoTuneOff"))
|
||||
toggle.steerActuatorDelay = np.clip(params.get_float("SteerDelay"), 0.01, 1.0) if advanced_lateral_tuning and tuning_level >= level["SteerDelay"] else steerActuatorDelay
|
||||
toggle.use_custom_steerActuatorDelay = bool(toggle.steerActuatorDelay != steerActuatorDelay)
|
||||
toggle.use_custom_steerActuatorDelay = bool(round(toggle.steerActuatorDelay, 2) != round(steerActuatorDelay, 2))
|
||||
toggle.friction = np.clip(params.get_float("SteerFriction"), 0, 0.5) if advanced_lateral_tuning and tuning_level >= level["SteerFriction"] else friction
|
||||
toggle.use_custom_friction = toggle.friction != friction and is_torque_car and not toggle.force_auto_tune or toggle.force_auto_tune_off
|
||||
toggle.use_custom_friction = bool(round(toggle.friction, 2) != round(friction, 2)) and is_torque_car and not toggle.force_auto_tune or toggle.force_auto_tune_off
|
||||
toggle.steerKp = [[0], [np.clip(params.get_float("SteerKP"), steerKp * 0.5, steerKp * 1.5) if advanced_lateral_tuning and is_torque_car and tuning_level >= level["SteerKP"] else steerKp]]
|
||||
toggle.latAccelFactor = np.clip(params.get_float("SteerLatAccel"), latAccelFactor * 0.75, latAccelFactor * 1.25) if advanced_lateral_tuning and tuning_level >= level["SteerLatAccel"] else latAccelFactor
|
||||
toggle.use_custom_latAccelFactor = toggle.latAccelFactor != latAccelFactor and is_torque_car and not toggle.force_auto_tune or toggle.force_auto_tune_off
|
||||
toggle.use_custom_latAccelFactor = bool(round(toggle.latAccelFactor, 2) != round(latAccelFactor, 2)) and is_torque_car and not toggle.force_auto_tune or toggle.force_auto_tune_off
|
||||
toggle.steerRatio = np.clip(params.get_float("SteerRatio"), steerRatio * 0.5, steerRatio * 1.5) if advanced_lateral_tuning and tuning_level >= level["SteerRatio"] else steerRatio
|
||||
toggle.use_custom_steerRatio = toggle.steerRatio != steerRatio and not toggle.force_auto_tune or toggle.force_auto_tune_off
|
||||
toggle.use_custom_steerRatio = bool(round(toggle.steerRatio, 2) != round(steerRatio, 2)) and not toggle.force_auto_tune or toggle.force_auto_tune_off
|
||||
|
||||
advanced_longitudinal_tuning = params.get_bool("AdvancedLongitudinalTune") if tuning_level >= level["AdvancedLongitudinalTune"] else default.get_bool("AdvancedLongitudinalTune")
|
||||
toggle.longitudinalActuatorDelay = np.clip(params.get_float("LongitudinalActuatorDelay"), 0, 1) if advanced_longitudinal_tuning and tuning_level >= level["LongitudinalActuatorDelay"] else longitudinalActuatorDelay
|
||||
@@ -632,7 +619,7 @@ class FrogPilotVariables:
|
||||
toggle.warningImmediate_volume = max(params.get_int("WarningImmediateVolume"), 25) if toggle.alert_volume_controller and tuning_level >= level["WarningImmediateVolume"] else default.get_int("WarningImmediateVolume")
|
||||
|
||||
toggle.always_on_lateral = params.get_bool("AlwaysOnLateral") if tuning_level >= level["AlwaysOnLateral"] else default.get_bool("AlwaysOnLateral")
|
||||
toggle.always_on_lateral_set = toggle.always_on_lateral and always_on_lateral_set
|
||||
toggle.always_on_lateral_set &= toggle.always_on_lateral
|
||||
toggle.always_on_lateral_lkas = toggle.always_on_lateral_set and toggle.use_lkas_for_aol and (params.get_bool("AlwaysOnLateralLKAS") if tuning_level >= level["AlwaysOnLateralLKAS"] else default.get_bool("AlwaysOnLateralLKAS"))
|
||||
toggle.always_on_lateral_main = toggle.always_on_lateral_set and not toggle.use_lkas_for_aol and (params.get_bool("AlwaysOnLateralMain") if tuning_level >= level["AlwaysOnLateralMain"] else default.get_bool("AlwaysOnLateralMain"))
|
||||
toggle.always_on_lateral_pause_speed = params.get_int("PauseAOLOnBrake") if toggle.always_on_lateral_set and tuning_level >= level["PauseAOLOnBrake"] else default.get_int("PauseAOLOnBrake")
|
||||
@@ -656,16 +643,11 @@ class FrogPilotVariables:
|
||||
toggle.conditional_navigation_lead = toggle.conditional_navigation and (params.get_bool("CENavigationLead") if tuning_level >= level["CENavigationLead"] else default.get_bool("CENavigationLead"))
|
||||
toggle.conditional_navigation_turns = toggle.conditional_navigation and (params.get_bool("CENavigationTurns") if tuning_level >= level["CENavigationTurns"] else default.get_bool("CENavigationTurns"))
|
||||
toggle.conditional_model_stop_time = params.get_int("CEModelStopTime") if toggle.conditional_experimental_mode and tuning_level >= level["CEModelStopTime"] else default.get_int("CEModelStopTime")
|
||||
toggle.conditional_signal = params.get_int("CESignalSpeed") if toggle.conditional_experimental_mode and tuning_level >= level["CESignalSpeed"] else default.get_int("CESignalSpeed")
|
||||
toggle.conditional_signal = params.get_int("CESignalSpeed") * speed_conversion if toggle.conditional_experimental_mode and tuning_level >= level["CESignalSpeed"] else default.get_int("CESignalSpeed") * CV.MPH_TO_MS
|
||||
toggle.conditional_signal_lane_detection = toggle.conditional_signal != 0 and (params.get_bool("CESignalLaneDetection") if tuning_level >= level["CESignalLaneDetection"] else default.get_bool("CESignalLaneDetection"))
|
||||
toggle.cem_status = toggle.conditional_experimental_mode and (params.get_bool("ShowCEMStatus") if tuning_level >= level["ShowCEMStatus"] else default.get_bool("ShowCEMStatus")) or toggle.debug_mode
|
||||
|
||||
toggle.curve_speed_controller = toggle.openpilot_longitudinal and (params.get_bool("CurveSpeedControl") if tuning_level >= level["CurveSpeedControl"] else default.get_bool("CurveSpeedControl"))
|
||||
toggle.curve_sensitivity = params.get_int("CurveSensitivity") / 100 if toggle.curve_speed_controller and tuning_level >= level["CurveSensitivity"] else default.get_int("CurveSensitivity") / 100
|
||||
toggle.turn_aggressiveness = params.get_int("TurnAggressiveness") / 100 if toggle.curve_speed_controller and tuning_level >= level["TurnAggressiveness"] else default.get_int("TurnAggressiveness") / 100
|
||||
toggle.map_turn_speed_controller = toggle.curve_speed_controller and (params.get_bool("MapTurnControl") if tuning_level >= level["MapTurnControl"] else default.get_bool("MapTurnControl"))
|
||||
toggle.mtsc_curvature_check = toggle.map_turn_speed_controller and (params.get_bool("MTSCCurvatureCheck") if tuning_level >= level["MTSCCurvatureCheck"] else default.get_bool("MTSCCurvatureCheck"))
|
||||
toggle.vision_turn_speed_controller = toggle.curve_speed_controller and (params.get_bool("VisionTurnControl") if tuning_level >= level["VisionTurnControl"] else default.get_bool("VisionTurnControl"))
|
||||
toggle.curve_speed_controller = toggle.openpilot_longitudinal and (params.get_bool("CurveSpeedController") if tuning_level >= level["CurveSpeedController"] else default.get_bool("CurveSpeedController"))
|
||||
toggle.csc_status = toggle.curve_speed_controller and (params.get_bool("ShowCSCStatus") if tuning_level >= level["ShowCSCStatus"] else default.get_bool("ShowCSCStatus")) or toggle.debug_mode
|
||||
|
||||
toggle.custom_alerts = params.get_bool("CustomAlerts") if tuning_level >= level["CustomAlerts"] else default.get_bool("CustomAlerts")
|
||||
@@ -682,28 +664,28 @@ class FrogPilotVariables:
|
||||
toggle.aggressive_jerk_danger = np.clip(params.get_int("AggressiveJerkDanger") / 100, 0.25, 2) if aggressive_profile and tuning_level >= level["AggressiveJerkDanger"] else default.get_int("AggressiveJerkDanger") / 100
|
||||
toggle.aggressive_jerk_speed = np.clip(params.get_int("AggressiveJerkSpeed") / 100, 0.25, 2) if aggressive_profile and tuning_level >= level["AggressiveJerkSpeed"] else default.get_int("AggressiveJerkSpeed") / 100
|
||||
toggle.aggressive_jerk_speed_decrease = np.clip(params.get_int("AggressiveJerkSpeedDecrease") / 100, 0.25, 2) if aggressive_profile and tuning_level >= level["AggressiveJerkSpeedDecrease"] else default.get_int("AggressiveJerkSpeedDecrease") / 100
|
||||
toggle.aggressive_follow = np.clip(params.get_float("AggressiveFollow"), 1, 5) if aggressive_profile and tuning_level >= level["AggressiveFollow"] else default.get_float("AggressiveFollow")
|
||||
toggle.aggressive_follow = np.clip(params.get_float("AggressiveFollow"), 1, MAX_T_FOLLOW) if aggressive_profile and tuning_level >= level["AggressiveFollow"] else default.get_float("AggressiveFollow")
|
||||
standard_profile = toggle.custom_personalities and (params.get_bool("StandardPersonalityProfile") if tuning_level >= level["StandardPersonalityProfile"] else default.get_bool("StandardPersonalityProfile"))
|
||||
toggle.standard_jerk_acceleration = np.clip(params.get_int("StandardJerkAcceleration") / 100, 0.25, 2) if standard_profile and tuning_level >= level["StandardJerkAcceleration"] else default.get_int("StandardJerkAcceleration") / 100
|
||||
toggle.standard_jerk_deceleration = np.clip(params.get_int("StandardJerkDeceleration") / 100, 0.25, 2) if standard_profile and tuning_level >= level["StandardJerkDeceleration"] else default.get_int("StandardJerkDeceleration") / 100
|
||||
toggle.standard_jerk_danger = np.clip(params.get_int("StandardJerkDanger") / 100, 0.25, 2) if standard_profile and tuning_level >= level["StandardJerkDanger"] else default.get_int("StandardJerkDanger") / 100
|
||||
toggle.standard_jerk_speed = np.clip(params.get_int("StandardJerkSpeed") / 100, 0.25, 2) if standard_profile and tuning_level >= level["StandardJerkSpeed"] else default.get_int("StandardJerkSpeed") / 100
|
||||
toggle.standard_jerk_speed_decrease = np.clip(params.get_int("StandardJerkSpeedDecrease") / 100, 0.25, 2) if standard_profile and tuning_level >= level["StandardJerkSpeedDecrease"] else default.get_int("StandardJerkSpeedDecrease") / 100
|
||||
toggle.standard_follow = np.clip(params.get_float("StandardFollow"), 1, 5) if standard_profile and tuning_level >= level["StandardFollow"] else default.get_float("StandardFollow")
|
||||
toggle.standard_follow = np.clip(params.get_float("StandardFollow"), 1, MAX_T_FOLLOW) if standard_profile and tuning_level >= level["StandardFollow"] else default.get_float("StandardFollow")
|
||||
relaxed_profile = toggle.custom_personalities and (params.get_bool("RelaxedPersonalityProfile") if tuning_level >= level["RelaxedPersonalityProfile"] else default.get_bool("RelaxedPersonalityProfile"))
|
||||
toggle.relaxed_jerk_acceleration = np.clip(params.get_int("RelaxedJerkAcceleration") / 100, 0.25, 2) if relaxed_profile and tuning_level >= level["RelaxedJerkAcceleration"] else default.get_int("RelaxedJerkAcceleration") / 100
|
||||
toggle.relaxed_jerk_deceleration = np.clip(params.get_int("RelaxedJerkDeceleration") / 100, 0.25, 2) if relaxed_profile and tuning_level >= level["RelaxedJerkDeceleration"] else default.get_int("RelaxedJerkDeceleration") / 100
|
||||
toggle.relaxed_jerk_danger = np.clip(params.get_int("RelaxedJerkDanger") / 100, 0.25, 2) if relaxed_profile and tuning_level >= level["RelaxedJerkDanger"] else default.get_int("RelaxedJerkDanger") / 100
|
||||
toggle.relaxed_jerk_speed = np.clip(params.get_int("RelaxedJerkSpeed") / 100, 0.25, 2) if relaxed_profile and tuning_level >= level["RelaxedJerkSpeed"] else default.get_int("RelaxedJerkSpeed") / 100
|
||||
toggle.relaxed_jerk_speed_decrease = np.clip(params.get_int("RelaxedJerkSpeedDecrease") / 100, 0.25, 2) if relaxed_profile and tuning_level >= level["RelaxedJerkSpeedDecrease"] else default.get_int("RelaxedJerkSpeedDecrease") / 100
|
||||
toggle.relaxed_follow = np.clip(params.get_float("RelaxedFollow"), 1, 5) if relaxed_profile and tuning_level >= level["RelaxedFollow"] else default.get_float("RelaxedFollow")
|
||||
toggle.relaxed_follow = np.clip(params.get_float("RelaxedFollow"), 1, MAX_T_FOLLOW) if relaxed_profile and tuning_level >= level["RelaxedFollow"] else default.get_float("RelaxedFollow")
|
||||
traffic_profile = toggle.custom_personalities and (params.get_bool("TrafficPersonalityProfile") if tuning_level >= level["TrafficPersonalityProfile"] else default.get_bool("TrafficPersonalityProfile"))
|
||||
toggle.traffic_mode_jerk_acceleration = [np.clip(params.get_int("TrafficJerkAcceleration") / 100, 0.25, 2) if traffic_profile and tuning_level >= level["TrafficJerkAcceleration"] else default.get_int("TrafficJerkAcceleration") / 100, toggle.aggressive_jerk_acceleration]
|
||||
toggle.traffic_mode_jerk_deceleration = [np.clip(params.get_int("TrafficJerkDeceleration") / 100, 0.25, 2) if traffic_profile and tuning_level >= level["TrafficJerkDeceleration"] else default.get_int("TrafficJerkDeceleration") / 100, toggle.aggressive_jerk_deceleration]
|
||||
toggle.traffic_mode_jerk_danger = [np.clip(params.get_int("TrafficJerkDanger") / 100, 0.25, 2) if traffic_profile and tuning_level >= level["TrafficJerkDanger"] else default.get_int("TrafficJerkDanger") / 100, toggle.aggressive_jerk_danger]
|
||||
toggle.traffic_mode_jerk_speed = [np.clip(params.get_int("TrafficJerkSpeed") / 100, 0.25, 2) if traffic_profile and tuning_level >= level["TrafficJerkSpeed"] else default.get_int("TrafficJerkSpeed") / 100, toggle.aggressive_jerk_speed]
|
||||
toggle.traffic_mode_jerk_speed_decrease = [np.clip(params.get_int("TrafficJerkSpeedDecrease") / 100, 0.25, 2) if traffic_profile and tuning_level >= level["TrafficJerkSpeedDecrease"] else default.get_int("TrafficJerkSpeedDecrease") / 100, toggle.aggressive_jerk_speed_decrease]
|
||||
toggle.traffic_mode_follow = [np.clip(params.get_float("TrafficFollow"), 0.5, 5) if traffic_profile and tuning_level >= level["TrafficFollow"] else default.get_float("TrafficFollow"), toggle.aggressive_follow]
|
||||
toggle.traffic_mode_follow = [np.clip(params.get_float("TrafficFollow"), 0.5, MAX_T_FOLLOW) if traffic_profile and tuning_level >= level["TrafficFollow"] else default.get_float("TrafficFollow"), toggle.aggressive_follow]
|
||||
|
||||
custom_ui = params.get_bool("CustomUI") if tuning_level >= level["CustomUI"] else default.get_bool("CustomUI")
|
||||
toggle.acceleration_path = toggle.openpilot_longitudinal and (custom_ui and (params.get_bool("AccelerationPath") if tuning_level >= level["AccelerationPath"] else default.get_bool("AccelerationPath")) or toggle.debug_mode)
|
||||
@@ -734,13 +716,13 @@ class FrogPilotVariables:
|
||||
toggle.storage_used_metrics = developer_metrics and (params.get_bool("ShowStorageUsed") if tuning_level >= level["ShowStorageUsed"] else default.get_bool("ShowStorageUsed")) and not toggle.debug_mode
|
||||
toggle.use_si_metrics = developer_metrics and (params.get_bool("UseSI") if tuning_level >= level["UseSI"] else default.get_bool("UseSI")) or toggle.debug_mode
|
||||
toggle.developer_sidebar = toggle.developer_ui and (params.get_bool("DeveloperSidebar") if tuning_level >= level["DeveloperSidebar"] else default.get_bool("DeveloperSidebar")) or toggle.debug_mode
|
||||
toggle.developer_sidebar_metric1 = params.get_int("DeveloperSidebarMetric1") if toggle.developer_sidebar and tuning_level >= level["DeveloperSidebarMetric1"] else 1 if toggle.debug_mode else default.get_float("DeveloperSidebarMetric1")
|
||||
toggle.developer_sidebar_metric2 = params.get_int("DeveloperSidebarMetric2") if toggle.developer_sidebar and tuning_level >= level["DeveloperSidebarMetric2"] else 3 if toggle.debug_mode else default.get_float("DeveloperSidebarMetric2")
|
||||
toggle.developer_sidebar_metric3 = params.get_int("DeveloperSidebarMetric3") if toggle.developer_sidebar and tuning_level >= level["DeveloperSidebarMetric3"] else 4 if toggle.debug_mode else default.get_float("DeveloperSidebarMetric3")
|
||||
toggle.developer_sidebar_metric4 = params.get_int("DeveloperSidebarMetric4") if toggle.developer_sidebar and tuning_level >= level["DeveloperSidebarMetric4"] else 5 if toggle.debug_mode else default.get_float("DeveloperSidebarMetric4")
|
||||
toggle.developer_sidebar_metric5 = params.get_int("DeveloperSidebarMetric5") if toggle.developer_sidebar and tuning_level >= level["DeveloperSidebarMetric5"] else 6 if toggle.debug_mode else default.get_float("DeveloperSidebarMetric5")
|
||||
toggle.developer_sidebar_metric6 = params.get_int("DeveloperSidebarMetric6") if toggle.developer_sidebar and tuning_level >= level["DeveloperSidebarMetric6"] else 7 if toggle.debug_mode else default.get_float("DeveloperSidebarMetric6")
|
||||
toggle.developer_sidebar_metric7 = params.get_int("DeveloperSidebarMetric7") if toggle.developer_sidebar and tuning_level >= level["DeveloperSidebarMetric7"] else 11 if toggle.debug_mode else default.get_float("DeveloperSidebarMetric7")
|
||||
toggle.developer_sidebar_metric1 = params.get_int("DeveloperSidebarMetric1") if toggle.developer_sidebar and tuning_level >= level["DeveloperSidebarMetric1"] else 1 if toggle.debug_mode else default.get_int("DeveloperSidebarMetric1")
|
||||
toggle.developer_sidebar_metric2 = params.get_int("DeveloperSidebarMetric2") if toggle.developer_sidebar and tuning_level >= level["DeveloperSidebarMetric2"] else 3 if toggle.debug_mode else default.get_int("DeveloperSidebarMetric2")
|
||||
toggle.developer_sidebar_metric3 = params.get_int("DeveloperSidebarMetric3") if toggle.developer_sidebar and tuning_level >= level["DeveloperSidebarMetric3"] else 4 if toggle.debug_mode else default.get_int("DeveloperSidebarMetric3")
|
||||
toggle.developer_sidebar_metric4 = params.get_int("DeveloperSidebarMetric4") if toggle.developer_sidebar and tuning_level >= level["DeveloperSidebarMetric4"] else 5 if toggle.debug_mode else default.get_int("DeveloperSidebarMetric4")
|
||||
toggle.developer_sidebar_metric5 = params.get_int("DeveloperSidebarMetric5") if toggle.developer_sidebar and tuning_level >= level["DeveloperSidebarMetric5"] else 6 if toggle.debug_mode else default.get_int("DeveloperSidebarMetric5")
|
||||
toggle.developer_sidebar_metric6 = params.get_int("DeveloperSidebarMetric6") if toggle.developer_sidebar and tuning_level >= level["DeveloperSidebarMetric6"] else 7 if toggle.debug_mode else default.get_int("DeveloperSidebarMetric6")
|
||||
toggle.developer_sidebar_metric7 = params.get_int("DeveloperSidebarMetric7") if toggle.developer_sidebar and tuning_level >= level["DeveloperSidebarMetric7"] else 11 if toggle.debug_mode else default.get_int("DeveloperSidebarMetric7")
|
||||
developer_widgets = toggle.developer_ui and params.get_bool("DeveloperWidgets") if tuning_level >= level["DeveloperWidgets"] else default.get_bool("DeveloperWidgets")
|
||||
toggle.adjacent_lead_tracking = has_radar and ((developer_widgets and params.get_bool("AdjacentLeadsUI") if tuning_level >= level["AdjacentLeadsUI"] else default.get_bool("AdjacentLeadsUI")) or toggle.debug_mode)
|
||||
toggle.radar_tracks = has_radar and ((developer_widgets and params.get_bool("RadarTracksUI") if tuning_level >= level["RadarTracksUI"] else default.get_bool("RadarTracksUI")) or toggle.debug_mode)
|
||||
@@ -798,7 +780,7 @@ class FrogPilotVariables:
|
||||
toggle.holiday_themes = params.get_bool("HolidayThemes") if tuning_level >= level["HolidayThemes"] else default.get_bool("HolidayThemes")
|
||||
toggle.current_holiday_theme = holiday_theme if toggle.holiday_themes else "stock"
|
||||
|
||||
toggle.lane_changes = params.get_bool("LaneChangeCustomizations") if tuning_level >= level["LaneChangeCustomizations"] else default.get_bool("LaneChangeCustomizations")
|
||||
toggle.lane_changes = params.get_bool("LaneChanges") if tuning_level >= level["LaneChanges"] else default.get_bool("LaneChanges")
|
||||
toggle.lane_change_delay = params.get_float("LaneChangeTime") if toggle.lane_changes and tuning_level >= level["LaneChangeTime"] else default.get_float("LaneChangeTime")
|
||||
toggle.lane_detection_width = params.get_float("LaneDetectionWidth") * distance_conversion if toggle.lane_changes and tuning_level >= level["LaneDetectionWidth"] else default.get_float("LaneDetectionWidth") * CV.FOOT_TO_METER
|
||||
toggle.lane_detection = toggle.lane_detection_width > 0
|
||||
@@ -807,6 +789,7 @@ class FrogPilotVariables:
|
||||
toggle.one_lane_change = toggle.lane_changes and (params.get_bool("OneLaneChange") if tuning_level >= level["OneLaneChange"] else default.get_bool("OneLaneChange"))
|
||||
|
||||
lateral_tuning = params.get_bool("LateralTune") if tuning_level >= level["LateralTune"] else default.get_bool("LateralTune")
|
||||
toggle.force_torque_controller = lateral_tuning and not is_torque_car and (params.get_bool("ForceTorqueController") if tuning_level >= level["ForceTorqueController"] else default.get_bool("ForceTorqueController"))
|
||||
toggle.nnff = lateral_tuning and has_nnff and not is_angle_car and (params.get_bool("NNFF") if tuning_level >= level["NNFF"] else default.get_bool("NNFF"))
|
||||
toggle.nnff_lite = not toggle.nnff and lateral_tuning and not is_angle_car and (params.get_bool("NNFFLite") if tuning_level >= level["NNFFLite"] else default.get_bool("NNFFLite"))
|
||||
toggle.use_turn_desires = lateral_tuning and (params.get_bool("TurnDesires") if tuning_level >= level["TurnDesires"] else default.get_bool("TurnDesires"))
|
||||
@@ -826,7 +809,6 @@ class FrogPilotVariables:
|
||||
|
||||
longitudinal_tuning = toggle.openpilot_longitudinal and (params.get_bool("LongitudinalTune") if tuning_level >= level["LongitudinalTune"] else default.get_bool("LongitudinalTune"))
|
||||
toggle.acceleration_profile = params.get_int("AccelerationProfile") if longitudinal_tuning and tuning_level >= level["AccelerationProfile"] else default.get_int("AccelerationProfile")
|
||||
toggle.sport_plus = max_acceleration_enabled and toggle.acceleration_profile == 3
|
||||
toggle.deceleration_profile = params.get_int("DecelerationProfile") if longitudinal_tuning and tuning_level >= level["DecelerationProfile"] else default.get_int("DecelerationProfile")
|
||||
toggle.human_acceleration = longitudinal_tuning and (params.get_bool("HumanAcceleration") if tuning_level >= level["HumanAcceleration"] else default.get_bool("HumanAcceleration"))
|
||||
toggle.human_following = longitudinal_tuning and (params.get_bool("HumanFollowing") if tuning_level >= level["HumanFollowing"] else default.get_bool("HumanFollowing"))
|
||||
@@ -834,39 +816,31 @@ class FrogPilotVariables:
|
||||
toggle.max_desired_acceleration = np.clip(params.get_float("MaxDesiredAcceleration"), 0.1, 4.0) if longitudinal_tuning and tuning_level >= level["MaxDesiredAcceleration"] else default.get_float("MaxDesiredAcceleration")
|
||||
toggle.taco_tune = longitudinal_tuning and (params.get_bool("TacoTune") if tuning_level >= level["TacoTune"] else default.get_bool("TacoTune"))
|
||||
|
||||
toggle.available_models = params.get("AvailableModels", encoding="utf-8") or ""
|
||||
toggle.available_model_names = params.get("AvailableModelNames", encoding="utf-8") or ""
|
||||
toggle.model_versions = params.get("ModelVersions", encoding="utf-8") or ""
|
||||
downloaded_models = [model for model in toggle.available_models.split(",") if any(MODELS_PATH.glob(f"{model}.*"))]
|
||||
toggle.model_randomizer = downloaded_models and (params.get_bool("ModelRandomizer") if tuning_level >= level["ModelRandomizer"] else default.get_bool("ModelRandomizer"))
|
||||
if toggle.available_models and toggle.available_model_names and downloaded_models and toggle.model_versions:
|
||||
toggle.available_models += f",{DEFAULT_TINYGRAD_MODEL}"
|
||||
toggle.available_model_names += f",{DEFAULT_TINYGRAD_MODEL_NAME}"
|
||||
toggle.model_versions += f",{DEFAULT_TINYGRAD_MODEL_VERSION}"
|
||||
downloaded_models += [DEFAULT_TINYGRAD_MODEL]
|
||||
if toggle.model_randomizer:
|
||||
if not started:
|
||||
blacklisted_models = (params.get("BlacklistedModels", encoding="utf-8") or "").split(",")
|
||||
selectable_models = [model for model in downloaded_models if model not in blacklisted_models]
|
||||
toggle.model = random.choice(selectable_models) if selectable_models else default.get("Model", encoding="utf-8")
|
||||
toggle.model_name = "Mystery Model 👻"
|
||||
toggle.model_version = toggle.model_versions.split(",")[toggle.available_models.split(",").index(toggle.model)]
|
||||
else:
|
||||
toggle.model = params.get("Model", encoding="utf-8") if tuning_level >= level["Model"] else default.get("Model", encoding="utf-8")
|
||||
if toggle.model in downloaded_models:
|
||||
toggle.model_name = toggle.available_model_names.split(",")[toggle.available_models.split(",").index(toggle.model)]
|
||||
toggle.model_version = toggle.model_versions.split(",")[toggle.available_models.split(",").index(toggle.model)]
|
||||
else:
|
||||
toggle.model = default.get("Model", encoding="utf-8")
|
||||
toggle.model_name = toggle.available_model_names.split(",")[toggle.available_models.split(",").index(toggle.model)]
|
||||
toggle.model_version = toggle.model_versions.split(",")[toggle.available_models.split(",").index(toggle.model)]
|
||||
toggle.available_models = (params.get("AvailableModels", encoding="utf-8") or "") + f",{DEFAULT_MODEL}"
|
||||
toggle.available_model_names = (params.get("AvailableModelNames", encoding="utf-8") or "") + f",{DEFAULT_MODEL_NAME}"
|
||||
downloaded_models = [model for model in toggle.available_models.split(",") if (MODELS_PATH / f"{model}.thneed").is_file() or all((MODELS_PATH / f"{model}_{filename}").is_file() for filename, _ in TINYGRAD_FILES)]
|
||||
model_versions = (params.get("ModelVersions", encoding="utf-8") or "") + f",{DEFAULT_MODEL_VERSION}"
|
||||
toggle.model_randomizer = params.get_bool("ModelRandomizer") if tuning_level >= level["ModelRandomizer"] else default.get_bool("ModelRandomizer")
|
||||
if toggle.model_randomizer:
|
||||
if not started:
|
||||
blacklisted_models = (params.get("BlacklistedModels", encoding="utf-8") or "").split(",")
|
||||
selectable_models = [model for model in downloaded_models if model not in blacklisted_models]
|
||||
toggle.model = random.choice(selectable_models) if selectable_models else DEFAULT_MODEL
|
||||
toggle.model_name = "Mystery Model 👻"
|
||||
toggle.model_version = model_versions.split(",")[toggle.available_models.split(",").index(toggle.model)]
|
||||
else:
|
||||
toggle.model = DEFAULT_CLASSIC_MODEL
|
||||
toggle.model_name = DEFAULT_CLASSIC_MODEL_NAME
|
||||
toggle.model_version = DEFAULT_CLASSIC_MODEL_VERSION
|
||||
model = ((params.get("Model", encoding="utf-8") if tuning_level >= level["Model"] else default.get("Model", encoding="utf-8")) or DEFAULT_MODEL).removesuffix("_default")
|
||||
if model in downloaded_models:
|
||||
toggle.model = model
|
||||
toggle.model_name = dict(zip(toggle.available_models.split(","), toggle.available_model_names.split(",")))[toggle.model]
|
||||
toggle.model_version = dict(zip(toggle.available_models.split(","), model_versions.split(",")))[toggle.model]
|
||||
else:
|
||||
toggle.model = DEFAULT_MODEL
|
||||
toggle.model_name = DEFAULT_MODEL_NAME
|
||||
toggle.model_version = DEFAULT_MODEL_VERSION
|
||||
toggle.classic_model = toggle.model_version in {"v1", "v2", "v3", "v4"}
|
||||
toggle.tinygrad_model = toggle.model_version in {"v7"}
|
||||
toggle.tomb_raider = toggle.model == "space-lab"
|
||||
toggle.classic_longitudinal = toggle.model_version in {"v1", "v2", "v3", "v4", "v5", "v6"}
|
||||
toggle.tinygrad_model = not toggle.classic_model and toggle.model_version not in {"v5", "v6"}
|
||||
|
||||
toggle.model_ui = params.get_bool("ModelUI") if tuning_level >= level["ModelUI"] else default.get_bool("ModelUI")
|
||||
toggle.dynamic_path_width = toggle.model_ui and (params.get_bool("DynamicPathWidth") if tuning_level >= level["DynamicPathWidth"] else default.get_bool("DynamicPathWidth"))
|
||||
@@ -884,8 +858,9 @@ class FrogPilotVariables:
|
||||
toggle.show_speed_limits = toggle.navigation_ui and (params.get_bool("ShowSpeedLimits") if tuning_level >= level["ShowSpeedLimits"] else default.get_bool("ShowSpeedLimits"))
|
||||
toggle.speed_limit_vienna = toggle.navigation_ui and (params.get_bool("UseVienna") if tuning_level >= level["UseVienna"] else default.get_bool("UseVienna"))
|
||||
|
||||
toggle.old_long_api = toggle.openpilot_longitudinal and toggle.car_make == "gm" and toggle.has_cc_long and not toggle.has_pedal
|
||||
toggle.old_long_api |= toggle.openpilot_longitudinal and toggle.car_make == "hyundai" and not (params.get_bool("NewLongAPI") if tuning_level >= level["NewLongAPI"] else default.get_bool("NewLongAPI"))
|
||||
if not started:
|
||||
toggle.old_long_api = toggle.openpilot_longitudinal and toggle.car_make == "gm" and toggle.has_cc_long and not toggle.has_pedal
|
||||
toggle.old_long_api |= toggle.openpilot_longitudinal and toggle.car_make == "hyundai" and not (params.get_bool("NewLongAPI") if tuning_level >= level["NewLongAPI"] else default.get_bool("NewLongAPI"))
|
||||
|
||||
personalize_openpilot = params.get_bool("PersonalizeOpenpilot") if tuning_level >= level["PersonalizeOpenpilot"] else default.get_bool("PersonalizeOpenpilot")
|
||||
toggle.color_scheme = toggle.current_holiday_theme if toggle.current_holiday_theme != "stock" else params.get("CustomColors", encoding="utf-8") if personalize_openpilot else "stock"
|
||||
@@ -894,10 +869,10 @@ class FrogPilotVariables:
|
||||
toggle.random_themes = personalize_openpilot and (params.get_bool("RandomThemes") if tuning_level >= level["RandomThemes"] else default.get_bool("RandomThemes"))
|
||||
toggle.signal_icons = toggle.current_holiday_theme if toggle.current_holiday_theme != "stock" else params.get("CustomSignals", encoding="utf-8") if personalize_openpilot else "stock"
|
||||
toggle.sound_pack = toggle.current_holiday_theme if toggle.current_holiday_theme != "stock" else params.get("CustomSounds", encoding="utf-8") if personalize_openpilot else "stock"
|
||||
if not toggle.random_themes or boot_run:
|
||||
if not toggle.random_themes:
|
||||
toggle.wheel_image = toggle.current_holiday_theme if toggle.current_holiday_theme != "stock" else params.get("WheelIcon", encoding="utf-8") if personalize_openpilot else "stock"
|
||||
else:
|
||||
toggle.wheel_image = next((file.resolve().stem for file in (ACTIVE_THEME_PATH / "steering_wheel").glob("wheel.*")), "none")
|
||||
toggle.wheel_image = next((file.resolve().stem for file in (ACTIVE_THEME_PATH / "steering_wheel").glob("wheel.*")), "stock")
|
||||
|
||||
quality_of_life_lateral = params.get_bool("QOLLateral") if tuning_level >= level["QOLLateral"] else default.get_bool("QOLLateral")
|
||||
toggle.pause_lateral_below_speed = params.get_int("PauseLateralSpeed") * speed_conversion if quality_of_life_lateral and tuning_level >= level["PauseLateralSpeed"] else default.get_int("PauseLateralSpeed") * CV.MPH_TO_MS
|
||||
@@ -906,9 +881,8 @@ class FrogPilotVariables:
|
||||
quality_of_life_longitudinal = params.get_bool("QOLLongitudinal") if tuning_level >= level["QOLLongitudinal"] else default.get_bool("QOLLongitudinal")
|
||||
toggle.cruise_increase = params.get_int("CustomCruise") if quality_of_life_longitudinal and not pcm_cruise and tuning_level >= level["CustomCruise"] else default.get_int("CustomCruise")
|
||||
toggle.cruise_increase_long = params.get_int("CustomCruiseLong") if quality_of_life_longitudinal and not pcm_cruise and tuning_level >= level["CustomCruiseLong"] else default.get_int("CustomCruiseLong")
|
||||
toggle.force_standstill = quality_of_life_longitudinal and (params.get_bool("ForceStandstill") if tuning_level >= level["ForceStandstill"] else default.get_bool("ForceStandstill"))
|
||||
toggle.force_stops = quality_of_life_longitudinal and (params.get_bool("ForceStops") if tuning_level >= level["ForceStops"] else default.get_bool("ForceStops"))
|
||||
toggle.increased_stopped_distance = params.get_int("IncreasedStoppedDistance") * distance_conversion if quality_of_life_longitudinal and tuning_level >= level["IncreasedStoppedDistance"] else default.get_int("IncreasedStoppedDistance") * CV.FOOT_TO_METER
|
||||
toggle.increase_stopped_distance = params.get_int("IncreasedStoppedDistance") * distance_conversion if quality_of_life_longitudinal and tuning_level >= level["IncreasedStoppedDistance"] else default.get_int("IncreasedStoppedDistance") * CV.FOOT_TO_METER
|
||||
map_gears = quality_of_life_longitudinal and (params.get_bool("MapGears") if tuning_level >= level["MapGears"] else default.get_bool("MapGears"))
|
||||
toggle.map_acceleration = map_gears and (params.get_bool("MapAcceleration") if tuning_level >= level["MapAcceleration"] else default.get_bool("MapAcceleration"))
|
||||
toggle.map_deceleration = map_gears and (params.get_bool("MapDeceleration") if tuning_level >= level["MapDeceleration"] else default.get_bool("MapDeceleration"))
|
||||
@@ -919,7 +893,6 @@ class FrogPilotVariables:
|
||||
toggle.camera_view = params.get_int("CameraView") if quality_of_life_visuals and tuning_level >= level["CameraView"] else default.get_int("CameraView")
|
||||
toggle.driver_camera_in_reverse = quality_of_life_visuals and (params.get_bool("DriverCamera") if tuning_level >= level["DriverCamera"] else default.get_bool("DriverCamera"))
|
||||
toggle.onroad_distance_button = toggle.openpilot_longitudinal and (quality_of_life_visuals and (params.get_bool("OnroadDistanceButton") if tuning_level >= level["OnroadDistanceButton"] else default.get_bool("OnroadDistanceButton")) or toggle.debug_mode)
|
||||
toggle.standby_mode = quality_of_life_visuals and (params.get_bool("StandbyMode") if tuning_level >= level["StandbyMode"] else default.get_bool("StandbyMode"))
|
||||
toggle.stopped_timer = quality_of_life_visuals and (params.get_bool("StoppedTimer") if tuning_level >= level["StoppedTimer"] else default.get_bool("StoppedTimer"))
|
||||
|
||||
toggle.rainbow_path = params.get_bool("RainbowPath") if tuning_level >= level["RainbowPath"] else default.get_bool("RainbowPath")
|
||||
@@ -932,6 +905,7 @@ class FrogPilotVariables:
|
||||
toggle.screen_recorder = screen_management and (params.get_bool("ScreenRecorder") if tuning_level >= level["ScreenRecorder"] else default.get_bool("ScreenRecorder")) or toggle.debug_mode
|
||||
toggle.screen_timeout = params.get_int("ScreenTimeout") if screen_management and tuning_level >= level["ScreenTimeout"] else default.get_int("ScreenTimeout")
|
||||
toggle.screen_timeout_onroad = params.get_int("ScreenTimeoutOnroad") if screen_management and tuning_level >= level["ScreenTimeoutOnroad"] else default.get_int("ScreenTimeoutOnroad")
|
||||
toggle.standby_mode = screen_management and (params.get_bool("StandbyMode") if tuning_level >= level["StandbyMode"] else default.get_bool("StandbyMode"))
|
||||
|
||||
toggle.sng_hack = toggle.openpilot_longitudinal and toggle.car_make == "toyota" and not toggle.has_pedal and not has_sng and (params.get_bool("SNGHack") if tuning_level >= level["SNGHack"] else default.get_bool("SNGHack"))
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
from cereal import car, custom
|
||||
from openpilot.selfdrive.controls.lib.drive_helpers import CRUISE_LONG_PRESS
|
||||
from openpilot.selfdrive.controls.lib.events import ET, EventName
|
||||
from openpilot.selfdrive.controls.lib.events import EventName
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_variables import NON_DRIVING_GEARS, params, params_memory
|
||||
|
||||
@@ -99,7 +99,7 @@ class FrogPilotCard:
|
||||
self.always_on_lateral_enabled &= sm["frogpilotPlan"].lateralCheck
|
||||
self.always_on_lateral_enabled &= sm["liveCalibration"].calPerc >= 1
|
||||
self.always_on_lateral_enabled &= not (carState.brakePressed and carState.vEgo < self.car.frogpilot_toggles.always_on_lateral_pause_speed) or carState.standstill
|
||||
self.always_on_lateral_enabled &= not any(getattr(event, ET.IMMEDIATE_DISABLE, False) for event in sm["onroadEvents"] if event.name != EventName.speedTooLow) or self.car.frogpilot_toggles.frogs_go_moo
|
||||
self.always_on_lateral_enabled &= not any(event.immediateDisable for events in (sm["onroadEvents"], sm["frogpilotOnroadEvents"]) for event in events if event.name != EventName.speedTooLow) or self.car.frogpilot_toggles.frogs_go_moo
|
||||
|
||||
if sm.updated["frogpilotPlan"] or any(be.type in (ButtonType.accelCruise, ButtonType.resumeCruise) for be in carState.buttonEvents):
|
||||
self.accel_pressed = any(be.type in (ButtonType.accelCruise, ButtonType.resumeCruise) for be in carState.buttonEvents)
|
||||
|
||||
@@ -8,7 +8,6 @@ from cereal import car, log
|
||||
from openpilot.common.conversions import Conversions as CV
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
|
||||
from openpilot.selfdrive.controls.lib.drive_helpers import V_CRUISE_MAX
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import A_CHANGE_COST, DANGER_ZONE_COST, J_EGO_COST, STOP_DISTANCE
|
||||
|
||||
@@ -21,17 +20,17 @@ from openpilot.frogpilot.controls.lib.frogpilot_following import FrogPilotFollow
|
||||
from openpilot.frogpilot.controls.lib.frogpilot_vcruise import FrogPilotVCruise
|
||||
|
||||
class FrogPilotPlanner:
|
||||
def __init__(self):
|
||||
def __init__(self, error_log, ThemeManager):
|
||||
self.cem = ConditionalExperimentalMode(self)
|
||||
self.frogpilot_acceleration = FrogPilotAcceleration(self)
|
||||
self.frogpilot_events = FrogPilotEvents(self)
|
||||
self.frogpilot_events = FrogPilotEvents(self, error_log, ThemeManager)
|
||||
self.frogpilot_following = FrogPilotFollowing(self)
|
||||
self.frogpilot_vcruise = FrogPilotVCruise(self)
|
||||
|
||||
with car.CarParams.from_bytes(params.get("CarParams", block=True)) as msg:
|
||||
self.CP = msg
|
||||
|
||||
self.tracking_lead_filter = FirstOrderFilter(0, 1, DT_MDL)
|
||||
self.tracking_lead_filter = FirstOrderFilter(0, 0.5, DT_MDL)
|
||||
|
||||
self.driving_in_curve = False
|
||||
self.lateral_check = False
|
||||
@@ -45,9 +44,10 @@ class FrogPilotPlanner:
|
||||
self.lateral_acceleration = 0
|
||||
self.model_length = 0
|
||||
self.road_curvature = 0
|
||||
self.time_to_curve = 0
|
||||
self.v_cruise = 0
|
||||
|
||||
def update(self, sm, frogpilot_toggles):
|
||||
def update(self, now, time_validated, sm, frogpilot_toggles):
|
||||
self.lead_one = sm["radarState"].leadOne
|
||||
|
||||
v_cruise = min(sm["controlsState"].vCruise, V_CRUISE_MAX) * CV.KPH_TO_MS
|
||||
@@ -104,23 +104,23 @@ class FrogPilotPlanner:
|
||||
self.model_stopped = self.model_length < CRUISING_SPEED * PLANNER_TIME
|
||||
self.model_stopped |= self.frogpilot_vcruise.forcing_stop
|
||||
|
||||
self.road_curvature = calculate_road_curvature(sm["modelV2"], v_ego)
|
||||
self.road_curvature, self.time_to_curve = calculate_road_curvature(sm["modelV2"], v_ego)
|
||||
|
||||
self.road_curvature_detected = (1 / abs(self.road_curvature))**0.5 < v_ego > CRUISING_SPEED and not (sm["carState"].leftBlinker or sm["carState"].rightBlinker)
|
||||
|
||||
if not sm["carState"].standstill:
|
||||
self.tracking_lead = self.update_lead_status()
|
||||
|
||||
self.v_cruise = self.frogpilot_vcruise.update(gps_position, v_cruise, v_ego, sm, frogpilot_toggles)
|
||||
self.v_cruise = self.frogpilot_vcruise.update(gps_position, now, time_validated, v_cruise, v_ego, sm, frogpilot_toggles)
|
||||
|
||||
def update_lead_status(self):
|
||||
following_lead = self.lead_one.status
|
||||
following_lead &= self.lead_one.dRel < self.model_length + STOP_DISTANCE
|
||||
|
||||
self.tracking_lead_filter.update(following_lead)
|
||||
return self.tracking_lead_filter.x >= THRESHOLD**2
|
||||
return self.tracking_lead_filter.x >= THRESHOLD
|
||||
|
||||
def publish(self, sm, pm, theme_updated, toggles_updated):
|
||||
def publish(self, theme_updated, toggles_updated, sm, pm, frogpilot_toggles):
|
||||
frogpilot_plan_send = messaging.new_message("frogpilotPlan")
|
||||
frogpilot_plan_send.valid = sm.all_checks(service_list=["carState", "controlsState"])
|
||||
frogpilotPlan = frogpilot_plan_send.frogpilotPlan
|
||||
@@ -132,6 +132,10 @@ class FrogPilotPlanner:
|
||||
frogpilotPlan.speedJerkStock = J_EGO_COST * self.frogpilot_following.base_speed_jerk
|
||||
frogpilotPlan.tFollow = self.frogpilot_following.t_follow
|
||||
|
||||
frogpilotPlan.cscControllingSpeed = self.frogpilot_vcruise.csc_controlling_speed
|
||||
frogpilotPlan.cscSpeed = self.frogpilot_vcruise.csc_target
|
||||
frogpilotPlan.cscTraining = self.frogpilot_vcruise.csc.enable_training
|
||||
|
||||
frogpilotPlan.desiredFollowDistance = self.frogpilot_following.desired_follow_distance
|
||||
|
||||
frogpilotPlan.experimentalMode = self.cem.experimental_mode or self.frogpilot_vcruise.slc.experimental_mode
|
||||
@@ -141,6 +145,8 @@ class FrogPilotPlanner:
|
||||
|
||||
frogpilotPlan.frogpilotEvents = self.frogpilot_events.events.to_msg()
|
||||
|
||||
frogpilotPlan.increasedStoppedDistance = frogpilot_toggles.increase_stopped_distance if not sm["frogpilotCarState"].trafficModeEnabled else 0
|
||||
|
||||
frogpilotPlan.laneWidthLeft = self.lane_width_left
|
||||
frogpilotPlan.laneWidthRight = self.lane_width_right
|
||||
|
||||
@@ -149,10 +155,6 @@ class FrogPilotPlanner:
|
||||
frogpilotPlan.maxAcceleration = self.frogpilot_acceleration.max_accel
|
||||
frogpilotPlan.minAcceleration = self.frogpilot_acceleration.min_accel
|
||||
|
||||
frogpilotPlan.mtscSpeed = self.frogpilot_vcruise.mtsc_target
|
||||
frogpilotPlan.vtscControllingCurve = self.frogpilot_vcruise.mtsc_target > self.frogpilot_vcruise.vtsc_target
|
||||
frogpilotPlan.vtscSpeed = self.frogpilot_vcruise.vtsc_target
|
||||
|
||||
frogpilotPlan.redLight = self.cem.stop_light_detected
|
||||
|
||||
frogpilotPlan.roadCurvature = self.road_curvature
|
||||
@@ -167,7 +169,7 @@ class FrogPilotPlanner:
|
||||
frogpilotPlan.speedLimitChanged = self.frogpilot_vcruise.slc.speed_limit_changed_timer > DT_MDL
|
||||
frogpilotPlan.unconfirmedSlcSpeedLimit = self.frogpilot_vcruise.slc.unconfirmed_speed_limit
|
||||
|
||||
frogpilotPlan.themeUpdated = theme_updated
|
||||
frogpilotPlan.themeUpdated = theme_updated or params_memory.get_bool("UseActiveTheme")
|
||||
|
||||
frogpilotPlan.togglesUpdated = toggles_updated
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ class ConditionalExperimentalMode:
|
||||
|
||||
self.curvature_filter = FirstOrderFilter(0, 1, DT_MDL)
|
||||
self.slow_lead_filter = FirstOrderFilter(0, 1, DT_MDL)
|
||||
self.stop_light_filter = FirstOrderFilter(0, 1, DT_MDL)
|
||||
self.stop_light_filter = FirstOrderFilter(0, 0.5, DT_MDL)
|
||||
|
||||
self.curve_detected = False
|
||||
self.experimental_mode = False
|
||||
@@ -24,7 +24,9 @@ class ConditionalExperimentalMode:
|
||||
|
||||
if self.status_value not in {1, 2} and not sm["carState"].standstill:
|
||||
self.update_conditions(v_ego, sm, frogpilot_toggles)
|
||||
|
||||
self.experimental_mode = self.check_conditions(v_ego, sm, frogpilot_toggles)
|
||||
|
||||
params_memory.put_int("CEStatus", self.status_value if self.experimental_mode else 0)
|
||||
else:
|
||||
self.experimental_mode = self.status_value == 2 or sm["carState"].standstill and self.experimental_mode and self.frogpilot_planner.model_stopped
|
||||
@@ -92,7 +94,7 @@ class ConditionalExperimentalMode:
|
||||
model_stopping = self.frogpilot_planner.model_length < v_ego * model_time
|
||||
|
||||
self.stop_light_filter.update(self.frogpilot_planner.model_stopped or model_stopping)
|
||||
self.stop_light_detected = self.stop_light_filter.x >= THRESHOLD**2 and not self.frogpilot_planner.tracking_lead
|
||||
self.stop_light_detected = self.stop_light_filter.x >= THRESHOLD and not self.frogpilot_planner.tracking_lead
|
||||
else:
|
||||
self.stop_light_filter.x = 0
|
||||
self.stop_light_detected = False
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import numpy as np
|
||||
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_variables import CRUISING_SPEED, DEFAULT_LATERAL_ACCELERATION, PLANNER_TIME, params
|
||||
|
||||
CALIBRATION_PROGRESS_THRESHOLD = 10 / DT_MDL
|
||||
MAX_CURVATURE = 0.1
|
||||
MIN_CURVATURE = 0.001
|
||||
PERCENTILE = 90
|
||||
ROUNDING_PRECISION = 5
|
||||
STEP = 0.001
|
||||
|
||||
class CurveSpeedController:
|
||||
def __init__(self, FrogPilotVCruise):
|
||||
self.frogpilot_planner = FrogPilotVCruise.frogpilot_planner
|
||||
|
||||
self.enable_training = False
|
||||
self.target_set = False
|
||||
|
||||
self.training_timer = 0
|
||||
|
||||
self.curvature_data = json.loads(params.get("CurvatureData") or "{}")
|
||||
|
||||
self.required_curvatures = [str(round(road_curvature, ROUNDING_PRECISION)) for road_curvature in np.arange(MIN_CURVATURE, MAX_CURVATURE + STEP, STEP)]
|
||||
|
||||
self.update_lateral_acceleration()
|
||||
|
||||
def log_data(self, v_ego, sm):
|
||||
self.enable_training = v_ego > CRUISING_SPEED
|
||||
self.enable_training &= not self.frogpilot_planner.tracking_lead
|
||||
self.enable_training &= not sm["carControl"].longActive
|
||||
|
||||
if self.enable_training:
|
||||
self.training_timer += DT_MDL
|
||||
|
||||
if self.training_timer >= PLANNER_TIME and self.frogpilot_planner.driving_in_curve and not (sm["carState"].leftBlinker or sm["carState"].rightBlinker):
|
||||
lateral_acceleration = abs(self.frogpilot_planner.lateral_acceleration)
|
||||
road_curvature = abs(round(self.frogpilot_planner.road_curvature, ROUNDING_PRECISION))
|
||||
|
||||
key = str(road_curvature)
|
||||
if key in self.curvature_data:
|
||||
data = self.curvature_data[key]
|
||||
|
||||
average = data["average"]
|
||||
count = data["count"]
|
||||
|
||||
self.curvature_data[key] = {
|
||||
"average": ((average * count) + lateral_acceleration) / (count + 1),
|
||||
"count": count + 1
|
||||
}
|
||||
else:
|
||||
self.curvature_data[key] = {
|
||||
"average": lateral_acceleration,
|
||||
"count": 1
|
||||
}
|
||||
|
||||
self.update_lateral_acceleration()
|
||||
else:
|
||||
self.enable_training = False
|
||||
|
||||
elif self.training_timer >= PLANNER_TIME:
|
||||
progress = 0.0
|
||||
for key in self.required_curvatures:
|
||||
if key in self.curvature_data:
|
||||
progress += min(self.curvature_data[key]["count"] / CALIBRATION_PROGRESS_THRESHOLD, 1.0)
|
||||
|
||||
params.put_float_nonblocking("CalibrationProgress", (progress / len(self.required_curvatures)) * 100)
|
||||
params.put_nonblocking("CurvatureData", json.dumps(self.curvature_data))
|
||||
|
||||
self.enable_training = False
|
||||
|
||||
self.training_timer = 0
|
||||
|
||||
else:
|
||||
self.enable_training = False
|
||||
|
||||
self.training_timer = 0
|
||||
|
||||
def update_lateral_acceleration(self):
|
||||
if self.curvature_data:
|
||||
all_samples = [data["average"] for data in self.curvature_data.values()]
|
||||
self.lateral_acceleration = float(np.percentile(all_samples, PERCENTILE))
|
||||
else:
|
||||
self.lateral_acceleration = DEFAULT_LATERAL_ACCELERATION
|
||||
|
||||
params.put_float_nonblocking("CalibratedLateralAcceleration", self.lateral_acceleration)
|
||||
|
||||
def update_target(self, v_ego):
|
||||
lateral_acceleration = self.lateral_acceleration
|
||||
|
||||
if self.target_set:
|
||||
csc_speed = (lateral_acceleration / abs(self.frogpilot_planner.road_curvature))**0.5
|
||||
decel_rate = (v_ego - csc_speed) / self.frogpilot_planner.time_to_curve
|
||||
|
||||
self.target -= decel_rate * DT_MDL
|
||||
self.target = float(np.clip(self.target, CRUISING_SPEED, csc_speed))
|
||||
else:
|
||||
self.target_set = True
|
||||
|
||||
self.target = v_ego
|
||||
@@ -1,18 +1,18 @@
|
||||
#!/usr/bin/env python3
|
||||
import numpy as np
|
||||
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_planner import A_CRUISE_MIN, get_max_accel
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import CRUISE_MIN_ACCEL
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_planner import ACCEL_MIN, get_max_accel
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_variables import CITY_SPEED_LIMIT
|
||||
|
||||
A_CRUISE_MIN_ECO = A_CRUISE_MIN / 2
|
||||
A_CRUISE_MIN_SPORT = A_CRUISE_MIN * 2
|
||||
A_CRUISE_MIN_ECO = CRUISE_MIN_ACCEL / 2
|
||||
A_CRUISE_MIN_SPORT = CRUISE_MIN_ACCEL * 2
|
||||
|
||||
# MPH = [0.0, 11, 22, 34, 45, 56, 89]
|
||||
A_CRUISE_MAX_BP_CUSTOM = [0.0, 5., 10., 15., 20., 25., 40.]
|
||||
A_CRUISE_MAX_VALS_ECO = [2.0, 1.5, 1.0, 0.8, 0.6, 0.4, 0.2]
|
||||
A_CRUISE_MAX_VALS_SPORT = [3.0, 2.5, 2.0, 1.5, 1.0, 0.8, 0.6]
|
||||
A_CRUISE_MAX_VALS_SPORT_PLUS = [4.0, 3.5, 3.0, 2.5, 2.0, 1.5, 1.0]
|
||||
# MPH = [0.0, 11, 22, 34, 45, 56, 89]
|
||||
A_CRUISE_MAX_BP_CUSTOM = [0.0, 5., 10., 15., 20., 25., 40.]
|
||||
A_CRUISE_MAX_VALS_ECO = [2.0, 1.5, 1.0, 0.8, 0.6, 0.4, 0.2]
|
||||
A_CRUISE_MAX_VALS_SPORT = [3.0, 2.5, 2.0, 1.5, 1.0, 0.8, 0.6]
|
||||
|
||||
def get_max_accel_eco(v_ego):
|
||||
return float(np.interp(v_ego, A_CRUISE_MAX_BP_CUSTOM, A_CRUISE_MAX_VALS_ECO))
|
||||
@@ -20,14 +20,11 @@ def get_max_accel_eco(v_ego):
|
||||
def get_max_accel_sport(v_ego):
|
||||
return float(np.interp(v_ego, A_CRUISE_MAX_BP_CUSTOM, A_CRUISE_MAX_VALS_SPORT))
|
||||
|
||||
def get_max_accel_sport_plus(v_ego):
|
||||
return float(np.interp(v_ego, A_CRUISE_MAX_BP_CUSTOM, A_CRUISE_MAX_VALS_SPORT_PLUS))
|
||||
|
||||
def get_max_accel_low_speeds(max_accel, v_cruise):
|
||||
return float(np.interp(v_cruise, [0., CITY_SPEED_LIMIT / 2, CITY_SPEED_LIMIT], [max_accel / 4, max_accel / 2, max_accel]))
|
||||
|
||||
def get_max_accel_ramp_off(max_accel, v_cruise, v_ego):
|
||||
return float(np.interp(v_cruise - v_ego, [0., 1., 5., 10.], [0., 0.5, 1.0, max_accel]))
|
||||
return float(np.interp(v_cruise - v_ego, [0., 1., 5.], [0., 0.5, max_accel]))
|
||||
|
||||
def get_max_allowed_accel(v_ego):
|
||||
return float(np.interp(v_ego, [0., 5., 20.], [4.0, 4.0, 2.0])) # ISO 15622:2018
|
||||
@@ -49,17 +46,17 @@ class FrogPilotAcceleration:
|
||||
if eco_gear:
|
||||
self.max_accel = get_max_accel_eco(v_ego)
|
||||
else:
|
||||
if frogpilot_toggles.sport_plus:
|
||||
self.max_accel = get_max_accel_sport_plus(v_ego)
|
||||
else:
|
||||
if frogpilot_toggles.acceleration_profile == 2:
|
||||
self.max_accel = get_max_accel_sport(v_ego)
|
||||
else:
|
||||
self.max_accel = get_max_allowed_accel(v_ego)
|
||||
else:
|
||||
if frogpilot_toggles.acceleration_profile == 1:
|
||||
self.max_accel = get_max_accel_eco(v_ego)
|
||||
elif frogpilot_toggles.acceleration_profile == 2:
|
||||
self.max_accel = get_max_accel_sport(v_ego)
|
||||
elif frogpilot_toggles.sport_plus:
|
||||
self.max_accel = get_max_accel_sport_plus(v_ego)
|
||||
elif frogpilot_toggles.acceleration_profile == 3:
|
||||
self.max_accel = get_max_allowed_accel(v_ego)
|
||||
else:
|
||||
self.max_accel = get_max_accel(v_ego)
|
||||
|
||||
@@ -67,7 +64,9 @@ class FrogPilotAcceleration:
|
||||
self.max_accel = min(get_max_accel_low_speeds(self.max_accel, self.frogpilot_planner.v_cruise), self.max_accel)
|
||||
self.max_accel = min(get_max_accel_ramp_off(self.max_accel, self.frogpilot_planner.v_cruise, v_ego), self.max_accel)
|
||||
|
||||
if sm["frogpilotCarState"].forceCoast:
|
||||
if self.frogpilot_planner.tracking_lead:
|
||||
self.min_accel = ACCEL_MIN
|
||||
elif sm["frogpilotCarState"].forceCoast:
|
||||
self.min_accel = A_CRUISE_MIN_ECO
|
||||
elif frogpilot_toggles.map_deceleration and (eco_gear or sport_gear):
|
||||
if eco_gear:
|
||||
@@ -80,4 +79,4 @@ class FrogPilotAcceleration:
|
||||
elif frogpilot_toggles.deceleration_profile == 2:
|
||||
self.min_accel = A_CRUISE_MIN_SPORT
|
||||
else:
|
||||
self.min_accel = A_CRUISE_MIN
|
||||
self.min_accel = CRUISE_MIN_ACCEL
|
||||
|
||||
@@ -4,48 +4,40 @@ import random
|
||||
from openpilot.common.conversions import Conversions as CV
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.selfdrive.controls.lib.desire_helper import TurnDirection
|
||||
from openpilot.selfdrive.controls.lib.events import ET, EVENTS, EventName, Events
|
||||
from openpilot.selfdrive.controls.lib.events import ET, EventName, FrogPilotEventName, Events
|
||||
from openpilot.selfdrive.controls.lib.vehicle_model import ACCELERATION_DUE_TO_GRAVITY
|
||||
|
||||
from openpilot.frogpilot.assets.theme_manager import update_wheel_image
|
||||
from openpilot.frogpilot.common.frogpilot_variables import CRUISING_SPEED, NON_DRIVING_GEARS, params, params_memory
|
||||
|
||||
DEJA_VU_G_FORCE = 0.5
|
||||
DEJA_VU_G_FORCE = 0.75
|
||||
RANDOM_EVENTS_CHANCE = 0.01 * DT_MDL
|
||||
|
||||
class FrogPilotEvents:
|
||||
def __init__(self, FrogPilotPlanner):
|
||||
def __init__(self, FrogPilotPlanner, error_log, ThemeManager):
|
||||
self.frogpilot_planner = FrogPilotPlanner
|
||||
self.theme_manager = ThemeManager
|
||||
|
||||
self.events = Events()
|
||||
self.error_log = error_log
|
||||
|
||||
self.events = Events(frogpilot=True)
|
||||
|
||||
self.accel30_played = False
|
||||
self.accel35_played = False
|
||||
self.accel40_played = False
|
||||
self.always_on_lateral_enabled_previously = False
|
||||
self.dejaVuCurve_played = False
|
||||
self.fcw_played = False
|
||||
self.firefoxSteerSaturated_played = False
|
||||
self.goatSteerSaturated_played = False
|
||||
self.hal9000_played = False
|
||||
self.holidayActive_played = False
|
||||
self.previous_traffic_mode = False
|
||||
self.random_event_playing = False
|
||||
self.startup_seen = False
|
||||
self.stopped_for_light = False
|
||||
self.thisIsFineSteerSaturated_played = False
|
||||
self.toBeContinued_played = False
|
||||
self.torqueNNLoad_played = False
|
||||
self.vCruise69_played = False
|
||||
self.yourFrogTriedToKillMe_played = False
|
||||
self.youveGotMail_played = False
|
||||
|
||||
self.max_acceleration = 0
|
||||
self.random_event_timer = 0
|
||||
self.tracking_lead_distance = 0
|
||||
self.tracked_lead_distance = 0
|
||||
|
||||
self.played_events = set()
|
||||
|
||||
def update(self, v_cruise, sm, frogpilot_toggles):
|
||||
alerts_empty = all(sm[state].alertText1 == "" and sm[state].alertText2 == "" for state in ["controlsState"])
|
||||
self.event_names = {event.name for event in sm["onroadEvents"]}
|
||||
self.frogpilot_event_names = {event.name for event in sm["frogpilotOnroadEvents"]}
|
||||
|
||||
alerts_empty = all(sm[state].alertText1 == "" and sm[state].alertText2 == "" for state in ["controlsState", "frogpilotControlsState"])
|
||||
|
||||
self.events.clear()
|
||||
|
||||
@@ -53,46 +45,50 @@ class FrogPilotEvents:
|
||||
self.random_event_timer += DT_MDL
|
||||
|
||||
if self.random_event_timer >= 5:
|
||||
update_wheel_image(frogpilot_toggles.wheel_image, frogpilot_toggles.current_holiday_theme, False)
|
||||
|
||||
self.theme_manager.update_wheel_image(frogpilot_toggles.wheel_image)
|
||||
params_memory.put_bool("UpdateWheelImage", True)
|
||||
|
||||
self.random_event_playing = False
|
||||
|
||||
self.random_event_timer = 0
|
||||
|
||||
if self.frogpilot_planner.frogpilot_vcruise.forcing_stop:
|
||||
self.events.add(EventName.forcingStop)
|
||||
if self.error_log.is_file():
|
||||
if frogpilot_toggles.random_events:
|
||||
self.events.add(FrogPilotEventName.openpilotCrashedRandomEvent)
|
||||
else:
|
||||
self.events.add(FrogPilotEventName.openpilotCrashed)
|
||||
|
||||
if not self.frogpilot_planner.tracking_lead and sm["carState"].standstill and sm["carState"].gearShifter not in NON_DRIVING_GEARS and frogpilot_toggles.green_light_alert:
|
||||
if not self.frogpilot_planner.model_stopped and self.stopped_for_light:
|
||||
self.events.add(EventName.greenLight)
|
||||
if self.frogpilot_planner.frogpilot_vcruise.forcing_stop:
|
||||
self.events.add(FrogPilotEventName.forcingStop)
|
||||
|
||||
if not self.frogpilot_planner.tracking_lead and sm["carState"].standstill and sm["carState"].gearShifter not in NON_DRIVING_GEARS:
|
||||
if not self.frogpilot_planner.model_stopped and self.stopped_for_light and frogpilot_toggles.green_light_alert:
|
||||
self.events.add(FrogPilotEventName.greenLight)
|
||||
|
||||
self.stopped_for_light = self.frogpilot_planner.cem.stop_light_detected
|
||||
else:
|
||||
self.stopped_for_light = False
|
||||
|
||||
if not self.holidayActive_played and self.startup_seen and alerts_empty and frogpilot_toggles.current_holiday_theme != "stock" and len(self.events) == 0:
|
||||
self.events.add(EventName.holidayActive)
|
||||
if "holidayActive" not in self.played_events and self.startup_seen and alerts_empty and frogpilot_toggles.current_holiday_theme != "stock" and len(self.events) == 0:
|
||||
self.events.add(FrogPilotEventName.holidayActive)
|
||||
|
||||
self.holidayActive_played = True
|
||||
self.played_events.add("holidayActive")
|
||||
|
||||
if self.frogpilot_planner.tracking_lead and sm["carState"].standstill and sm["carState"].gearShifter not in NON_DRIVING_GEARS and frogpilot_toggles.lead_departing_alert:
|
||||
if self.tracking_lead_distance == 0:
|
||||
self.tracking_lead_distance = self.frogpilot_planner.lead_one.dRel
|
||||
if self.tracked_lead_distance == 0:
|
||||
self.tracked_lead_distance = self.frogpilot_planner.lead_one.dRel
|
||||
|
||||
lead_departing = self.frogpilot_planner.lead_one.dRel - self.tracking_lead_distance > 1
|
||||
lead_departing = self.frogpilot_planner.lead_one.dRel - self.tracked_lead_distance > 1
|
||||
lead_departing &= self.frogpilot_planner.lead_one.vLead > 1
|
||||
|
||||
if lead_departing:
|
||||
self.events.add(EventName.leadDeparting)
|
||||
self.events.add(FrogPilotEventName.leadDeparting)
|
||||
else:
|
||||
self.tracking_lead_distance = 0
|
||||
self.tracked_lead_distance = 0
|
||||
|
||||
if not self.torqueNNLoad_played and self.startup_seen and alerts_empty and len(self.events) == 0 and params.get("NNFFModelName", encoding="utf-8") is not None and frogpilot_toggles.nnff:
|
||||
self.events.add(EventName.torqueNNLoad)
|
||||
if "torqueNNLoad" not in self.played_events and self.startup_seen and alerts_empty and len(self.events) == 0 and params.get("NNFFModelName", encoding="utf-8") is not None and frogpilot_toggles.nnff:
|
||||
self.events.add(FrogPilotEventName.torqueNNLoad)
|
||||
|
||||
self.torqueNNLoad_played = True
|
||||
self.played_events.add("torqueNNLoad")
|
||||
|
||||
if not self.random_event_playing and frogpilot_toggles.random_events:
|
||||
acceleration = sm["carState"].aEgo
|
||||
@@ -102,151 +98,125 @@ class FrogPilotEvents:
|
||||
else:
|
||||
self.max_acceleration = 0
|
||||
|
||||
if not self.accel30_played and 3.5 > self.max_acceleration >= 3.0 and acceleration < 1.5:
|
||||
self.events.add(EventName.accel30)
|
||||
|
||||
update_wheel_image("weeb_wheel")
|
||||
if "accel30" not in self.played_events and 3.5 > self.max_acceleration >= 3.0 and acceleration < 1.5:
|
||||
self.events.add(FrogPilotEventName.accel30)
|
||||
|
||||
self.theme_manager.update_wheel_image("weeb_wheel", random_event=True)
|
||||
params_memory.put_bool("UpdateWheelImage", True)
|
||||
|
||||
self.accel30_played = True
|
||||
self.random_event_playing = True
|
||||
|
||||
self.max_acceleration = 0
|
||||
self.played_events.add("accel30")
|
||||
|
||||
elif not self.accel35_played and 4.0 > self.max_acceleration >= 3.5 and acceleration < 1.5:
|
||||
self.events.add(EventName.accel35)
|
||||
|
||||
update_wheel_image("tree_fiddy")
|
||||
elif "accel35" not in self.played_events and 4.0 > self.max_acceleration >= 3.5 and acceleration < 1.5:
|
||||
self.events.add(FrogPilotEventName.accel35)
|
||||
|
||||
self.theme_manager.update_wheel_image("tree_fiddy", random_event=True)
|
||||
params_memory.put_bool("UpdateWheelImage", True)
|
||||
|
||||
self.accel35_played = True
|
||||
self.random_event_playing = True
|
||||
|
||||
self.max_acceleration = 0
|
||||
self.played_events.add("accel35")
|
||||
|
||||
elif not self.accel40_played and self.max_acceleration >= 4.0 and acceleration < 1.5:
|
||||
self.events.add(EventName.accel40)
|
||||
|
||||
update_wheel_image("great_scott")
|
||||
elif "accel40" not in self.played_events and self.max_acceleration >= 4.0 and acceleration < 1.5:
|
||||
self.events.add(FrogPilotEventName.accel40)
|
||||
|
||||
self.theme_manager.update_wheel_image("great_scott", random_event=True)
|
||||
params_memory.put_bool("UpdateWheelImage", True)
|
||||
|
||||
self.accel40_played = True
|
||||
self.random_event_playing = True
|
||||
|
||||
self.max_acceleration = 0
|
||||
self.played_events.add("accel40")
|
||||
|
||||
if not self.dejaVuCurve_played and sm["carState"].vEgo > CRUISING_SPEED:
|
||||
if "dejaVuCurve" not in self.played_events and sm["carState"].vEgo > CRUISING_SPEED:
|
||||
if self.frogpilot_planner.lateral_acceleration >= DEJA_VU_G_FORCE * ACCELERATION_DUE_TO_GRAVITY:
|
||||
self.events.add(EventName.dejaVuCurve)
|
||||
self.events.add(FrogPilotEventName.dejaVuCurve)
|
||||
|
||||
self.dejaVuCurve_played = True
|
||||
self.random_event_playing = True
|
||||
self.played_events.add("dejaVuCurve")
|
||||
|
||||
if not self.hal9000_played and sm["controlsState"].alertType == ET.NO_ENTRY:
|
||||
self.events.add(EventName.hal9000)
|
||||
if "hal9000" not in self.played_events and (sm["controlsState"].alertType == ET.NO_ENTRY or sm["frogpilotControlsState"].alertType == ET.NO_ENTRY):
|
||||
self.events.add(FrogPilotEventName.hal9000)
|
||||
|
||||
self.hal9000_played = True
|
||||
self.random_event_playing = True
|
||||
self.played_events.add("hal9000")
|
||||
|
||||
saturated_events = [
|
||||
("controlsState", [EventName.steerSaturated], EVENTS),
|
||||
("controlsState", [EventName.goatSteerSaturated], EVENTS)
|
||||
]
|
||||
saturated_alert_match = any(sm[state].alertText2 == events[event][ET.WARNING].alert_text_2 for state, event_group, events in saturated_events for event in event_group)
|
||||
if saturated_alert_match:
|
||||
if (EventName.steerSaturated in self.event_names or FrogPilotEventName.goatSteerSaturated in self.frogpilot_event_names):
|
||||
event_choices = []
|
||||
if not self.firefoxSteerSaturated_played:
|
||||
if "firefoxSteerSaturated" not in self.played_events:
|
||||
event_choices.append("firefoxSteerSaturated")
|
||||
if not self.goatSteerSaturated_played:
|
||||
if "goatSteerSaturated" not in self.played_events:
|
||||
event_choices.append("goatSteerSaturated")
|
||||
if not self.thisIsFineSteerSaturated_played:
|
||||
if "thisIsFineSteerSaturated" not in self.played_events:
|
||||
event_choices.append("thisIsFineSteerSaturated")
|
||||
|
||||
if event_choices and random.random() < RANDOM_EVENTS_CHANCE:
|
||||
event_choice = random.choice(event_choices)
|
||||
|
||||
if event_choice == "firefoxSteerSaturated":
|
||||
self.events.add(EventName.firefoxSteerSaturated)
|
||||
|
||||
update_wheel_image("firefox")
|
||||
self.events.add(FrogPilotEventName.firefoxSteerSaturated)
|
||||
|
||||
self.theme_manager.update_wheel_image("firefox", random_event=True)
|
||||
params_memory.put_bool("UpdateWheelImage", True)
|
||||
|
||||
self.firefoxSteerSaturated_played = True
|
||||
|
||||
elif event_choice == "goatSteerSaturated":
|
||||
self.events.add(EventName.goatSteerSaturated)
|
||||
|
||||
update_wheel_image("goat")
|
||||
self.events.add(FrogPilotEventName.goatSteerSaturated)
|
||||
|
||||
self.theme_manager.update_wheel_image("goat", random_event=True)
|
||||
params_memory.put_bool("UpdateWheelImage", True)
|
||||
|
||||
self.goatSteerSaturated_played = True
|
||||
|
||||
elif event_choice == "thisIsFineSteerSaturated":
|
||||
self.events.add(EventName.thisIsFineSteerSaturated)
|
||||
|
||||
update_wheel_image("this_is_fine")
|
||||
self.events.add(FrogPilotEventName.thisIsFineSteerSaturated)
|
||||
|
||||
self.theme_manager.update_wheel_image("this_is_fine", random_event=True)
|
||||
params_memory.put_bool("UpdateWheelImage", True)
|
||||
|
||||
self.thisIsFineSteerSaturated_played = True
|
||||
|
||||
self.random_event_playing = True
|
||||
self.played_events.add(event_choice)
|
||||
|
||||
if not self.vCruise69_played and 70 > max(sm["controlsState"].vCruise, sm["controlsState"].vCruiseCluster) * (1 if frogpilot_toggles.is_metric else CV.KPH_TO_MPH) >= 69:
|
||||
self.events.add(EventName.vCruise69)
|
||||
if "vCruise69" not in self.played_events and 70 > max(sm["controlsState"].vCruise, sm["controlsState"].vCruiseCluster) * (1 if frogpilot_toggles.is_metric else CV.KPH_TO_MPH) >= 69:
|
||||
self.events.add(FrogPilotEventName.vCruise69)
|
||||
|
||||
self.vCruise69_played = True
|
||||
self.random_event_playing = True
|
||||
self.played_events.add("vCruise69")
|
||||
|
||||
fcw_alert_match = sm["controlsState"].alertText1 == EVENTS[EventName.fcw][ET.PERMANENT].alert_text_1 and sm["controlsState"].alertText2 == EVENTS[EventName.fcw][ET.PERMANENT].alert_text_2
|
||||
stock_aeb_alert_match = sm["controlsState"].alertText1 == EVENTS[EventName.stockAeb][ET.PERMANENT].alert_text_1 and sm["controlsState"].alertText2 == EVENTS[EventName.stockAeb][ET.PERMANENT].alert_text_2
|
||||
if fcw_alert_match or stock_aeb_alert_match:
|
||||
event_choices = []
|
||||
if not self.toBeContinued_played:
|
||||
event_choices.append("toBeContinued")
|
||||
if not self.yourFrogTriedToKillMe_played:
|
||||
event_choices.append("yourFrogTriedToKillMe")
|
||||
if (EventName.fcw in self.event_names or EventName.stockAeb in self.event_names):
|
||||
event_choices = []
|
||||
if "toBeContinued" not in self.played_events:
|
||||
event_choices.append("toBeContinued")
|
||||
if "yourFrogTriedToKillMe" not in self.played_events:
|
||||
event_choices.append("yourFrogTriedToKillMe")
|
||||
|
||||
event_choice = random.choice(event_choices)
|
||||
if event_choice == "toBeContinued":
|
||||
self.events.add(EventName.toBeContinued)
|
||||
event_choice = random.choice(event_choices)
|
||||
if event_choice == "toBeContinued":
|
||||
self.events.add(FrogPilotEventName.toBeContinued)
|
||||
elif event_choice == "yourFrogTriedToKillMe":
|
||||
self.events.add(FrogPilotEventName.yourFrogTriedToKillMe)
|
||||
|
||||
self.toBeContinued_played = True
|
||||
self.random_event_playing = True
|
||||
self.played_events.add(event_choice)
|
||||
|
||||
elif event_choice == "yourFrogTriedToKillMe":
|
||||
self.events.add(EventName.yourFrogTriedToKillMe)
|
||||
|
||||
self.yourFrogTriedToKillMe_played = True
|
||||
|
||||
self.random_event_playing = True
|
||||
|
||||
if not self.youveGotMail_played and sm["frogpilotCarState"].alwaysOnLateralEnabled and not self.always_on_lateral_enabled_previously:
|
||||
if "youveGotMail" not in self.played_events and sm["frogpilotCarState"].alwaysOnLateralEnabled and not self.always_on_lateral_enabled_previously:
|
||||
if random.random() < RANDOM_EVENTS_CHANCE:
|
||||
self.events.add(EventName.youveGotMail)
|
||||
self.events.add(FrogPilotEventName.youveGotMail)
|
||||
|
||||
self.youveGotMail_played = True
|
||||
self.random_event_playing = True
|
||||
self.played_events.add("youveGotMail")
|
||||
|
||||
self.always_on_lateral_enabled_previously = sm["frogpilotCarState"].alwaysOnLateralEnabled
|
||||
|
||||
if frogpilot_toggles.speed_limit_changed_alert and self.frogpilot_planner.frogpilot_vcruise.slc.speed_limit_changed_timer == DT_MDL:
|
||||
self.events.add(EventName.speedLimitChanged)
|
||||
self.events.add(FrogPilotEventName.speedLimitChanged)
|
||||
|
||||
self.startup_seen |= sm["controlsState"].alertText1 == frogpilot_toggles.startup_alert_top and sm["controlsState"].alertText2 == frogpilot_toggles.startup_alert_bottom
|
||||
self.startup_seen |= sm["frogpilotControlsState"].alertText1 == frogpilot_toggles.startup_alert_top and sm["frogpilotControlsState"].alertText2 == frogpilot_toggles.startup_alert_bottom
|
||||
|
||||
if sm["frogpilotCarState"].trafficModeEnabled != self.previous_traffic_mode:
|
||||
if self.previous_traffic_mode:
|
||||
self.events.add(EventName.trafficModeInactive)
|
||||
self.events.add(FrogPilotEventName.trafficModeInactive)
|
||||
else:
|
||||
self.events.add(EventName.trafficModeActive)
|
||||
self.events.add(FrogPilotEventName.trafficModeActive)
|
||||
|
||||
self.previous_traffic_mode = sm["frogpilotCarState"].trafficModeEnabled
|
||||
|
||||
if sm["modelV2"].meta.turnDirection == TurnDirection.turnLeft:
|
||||
self.events.add(EventName.turningLeft)
|
||||
elif sm["modelV2"].meta.turnDirection == TurnDirection.turnRight:
|
||||
self.events.add(EventName.turningRight)
|
||||
if sm["frogpilotModelV2"].turnDirection == TurnDirection.turnLeft:
|
||||
self.events.add(FrogPilotEventName.turningLeft)
|
||||
elif sm["frogpilotModelV2"].turnDirection == TurnDirection.turnRight:
|
||||
self.events.add(FrogPilotEventName.turningRight)
|
||||
|
||||
@@ -1,71 +1,167 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
|
||||
from openpilot.common.conversions import Conversions as CV
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.selfdrive.controls.controlsd import EventName, FrogPilotEventName, State
|
||||
from openpilot.selfdrive.controls.lib.drive_helpers import V_CRUISE_MAX
|
||||
from openpilot.selfdrive.ui.soundd import FrogPilotAudibleAlert
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_variables import params, params_tracking
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import clean_model_name
|
||||
from openpilot.frogpilot.common.frogpilot_variables import params
|
||||
|
||||
RANDOM_EVENTS = {
|
||||
FrogPilotEventName.accel30: "accel30",
|
||||
FrogPilotEventName.accel35: "accel35",
|
||||
FrogPilotEventName.accel40: "accel40",
|
||||
FrogPilotEventName.dejaVuCurve: "dejaVuCurve",
|
||||
FrogPilotEventName.firefoxSteerSaturated: "firefoxSteerSaturated",
|
||||
FrogPilotEventName.hal9000: "hal9000",
|
||||
FrogPilotEventName.openpilotCrashedRandomEvent: "openpilotCrashedRandomEvent",
|
||||
FrogPilotEventName.thisIsFineSteerSaturated: "thisIsFineSteerSaturated",
|
||||
FrogPilotEventName.toBeContinued: "toBeContinued",
|
||||
FrogPilotEventName.vCruise69: "vCruise69",
|
||||
FrogPilotEventName.yourFrogTriedToKillMe: "yourFrogTriedToKillMe",
|
||||
FrogPilotEventName.youveGotMail: "youveGotMail",
|
||||
}
|
||||
|
||||
class FrogPilotTracking:
|
||||
def __init__(self):
|
||||
self.frogpilot_stats = json.loads(params.get("FrogPilotStats") or "{}")
|
||||
def __init__(self, frogpilot_planner, frogpilot_toggles):
|
||||
self.frogpilot_events = frogpilot_planner.frogpilot_events
|
||||
|
||||
self.total_drives = params_tracking.get_int("FrogPilotDrives")
|
||||
self.total_kilometers = params_tracking.get_float("FrogPilotKilometers")
|
||||
self.total_minutes = params_tracking.get_float("FrogPilotMinutes")
|
||||
self.frogpilot_stats = json.loads(params.get("FrogPilotStats") or "{}")
|
||||
self.frogpilot_stats.setdefault("AOLTime", self.frogpilot_stats.get("TotalAOLTime", 0))
|
||||
self.frogpilot_stats.setdefault("LateralTime", self.frogpilot_stats.get("TotalLateralTime", 0))
|
||||
self.frogpilot_stats.setdefault("LongitudinalTime", self.frogpilot_stats.get("TotalLongitudinalTime", 0))
|
||||
self.frogpilot_stats.setdefault("TrackedTime", self.frogpilot_stats.get("TotalTrackedTime", 0))
|
||||
|
||||
self.frogpilot_stats = {key: value for key, value in self.frogpilot_stats.items() if not key.startswith("Total")}
|
||||
|
||||
params.put("FrogPilotStats", json.dumps(self.frogpilot_stats))
|
||||
|
||||
self.drive_added = False
|
||||
self.enabled = False
|
||||
|
||||
self.aol_engaged_time = 0
|
||||
self.drive_distance = 0
|
||||
self.drive_time = 0
|
||||
self.lateral_engaged_time = 0
|
||||
self.longitudinal_engaged_time = 0
|
||||
self.distance_since_override = 0
|
||||
self.tracked_time = 0
|
||||
|
||||
self.total_aol_engaged = self.frogpilot_stats.get("TotalAOLTime", 0)
|
||||
self.total_lateral_engaged = self.frogpilot_stats.get("TotalLateralTime", 0)
|
||||
self.total_longitudinal_engaged = self.frogpilot_stats.get("TotalLongitudinalTime", 0)
|
||||
self.total_tracked_time = self.frogpilot_stats.get("TotalTrackedTime", 0)
|
||||
self.previous_events = set()
|
||||
self.previous_random_events = set()
|
||||
|
||||
self.sound = FrogPilotAudibleAlert.none
|
||||
self.state = State.disabled
|
||||
|
||||
self.model_name = clean_model_name(dict(zip(frogpilot_toggles.available_models.split(","), frogpilot_toggles.available_model_names.split(",")))[frogpilot_toggles.model])
|
||||
|
||||
def update(self, now, time_validated, sm, frogpilot_toggles):
|
||||
v_cruise = min(sm["controlsState"].vCruiseCluster, V_CRUISE_MAX) * CV.KPH_TO_MS
|
||||
v_ego = max(sm["carState"].vEgo, 0)
|
||||
|
||||
def update(self, sm):
|
||||
self.enabled |= sm["controlsState"].enabled or sm["frogpilotCarState"].alwaysOnLateralEnabled
|
||||
|
||||
self.drive_distance += sm["carState"].vEgo * DT_MDL
|
||||
self.drive_time += DT_MDL
|
||||
self.frogpilot_stats["FrogPilotMeters"] = self.frogpilot_stats.get("FrogPilotMeters", 0) + (v_ego * DT_MDL)
|
||||
|
||||
self.tracked_time += DT_MDL
|
||||
|
||||
self.frogpilot_stats["CurrentMonthsKilometers"] = self.frogpilot_stats.get("CurrentMonthsKilometers", 0) + (v_ego * DT_MDL) / 1000
|
||||
|
||||
self.frogpilot_stats["HighestAcceleration"] = max(self.frogpilot_events.max_acceleration, self.frogpilot_stats.get("HighestAcceleration", 0))
|
||||
|
||||
if sm["frogpilotControlsState"].alertSound != self.sound:
|
||||
if sm["frogpilotControlsState"].alertSound == FrogPilotAudibleAlert.goat:
|
||||
self.frogpilot_stats["GoatScreams"] = self.frogpilot_stats.get("GoatScreams", 0) + 1
|
||||
|
||||
self.sound = sm["frogpilotControlsState"].alertSound
|
||||
|
||||
if sm["controlsState"].enabled:
|
||||
key = str(round(v_cruise, 2))
|
||||
total_cruise_speed_times = self.frogpilot_stats.get("CruiseSpeedTimes", {})
|
||||
total_cruise_speed_times[key] = total_cruise_speed_times.get(key, 0) + DT_MDL
|
||||
self.frogpilot_stats["CruiseSpeedTimes"] = total_cruise_speed_times
|
||||
|
||||
if sm["carControl"].latActive:
|
||||
self.lateral_engaged_time += DT_MDL
|
||||
self.frogpilot_stats["LateralTime"] = self.frogpilot_stats.get("LateralTime", 0) + DT_MDL
|
||||
if sm["carControl"].longActive:
|
||||
self.longitudinal_engaged_time += DT_MDL
|
||||
self.frogpilot_stats["LongitudinalTime"] = self.frogpilot_stats.get("LongitudinalTime", 0) + DT_MDL
|
||||
elif sm["frogpilotCarState"].alwaysOnLateralEnabled:
|
||||
self.aol_engaged_time += DT_MDL
|
||||
self.frogpilot_stats["AOLTime"] = self.frogpilot_stats.get("AOLTime", 0) + DT_MDL
|
||||
|
||||
if self.drive_time > 60 and sm["carState"].standstill and self.enabled:
|
||||
self.total_kilometers += self.drive_distance / 1000
|
||||
params_tracking.put_float_nonblocking("FrogPilotKilometers", self.total_kilometers)
|
||||
self.drive_distance = 0
|
||||
if sm["carState"].standstill:
|
||||
self.frogpilot_stats["StandstillTime"] = self.frogpilot_stats.get("StandstillTime", 0) + DT_MDL
|
||||
if self.frogpilot_events.stopped_for_light:
|
||||
self.frogpilot_stats["StopLightTime"] = self.frogpilot_stats.get("StopLightTime", 0) + DT_MDL
|
||||
|
||||
self.total_minutes += self.drive_time / 60
|
||||
params_tracking.put_float_nonblocking("FrogPilotMinutes", self.total_minutes)
|
||||
if sm["controlsState"].experimentalMode:
|
||||
self.frogpilot_stats["ExperimentalModeTime"] = self.frogpilot_stats.get("ExperimentalModeTime", 0) + DT_MDL
|
||||
|
||||
self.total_aol_engaged += self.aol_engaged_time
|
||||
self.total_lateral_engaged += self.lateral_engaged_time
|
||||
self.total_longitudinal_engaged += self.longitudinal_engaged_time
|
||||
self.total_tracked_time += self.drive_time
|
||||
if sm["controlsState"].state in (State.disabled, State.overriding):
|
||||
self.distance_since_override = 0
|
||||
self.frogpilot_stats["OverrideTime"] = self.frogpilot_stats.get("OverrideTime", 0) + DT_MDL
|
||||
else:
|
||||
self.distance_since_override += v_ego * DT_MDL
|
||||
self.frogpilot_stats["LongestDistanceWithoutOverride"] = max(self.distance_since_override, self.frogpilot_stats.get("LongestDistanceWithoutOverride", 0))
|
||||
|
||||
self.frogpilot_stats["TotalAOLTime"] = self.total_aol_engaged
|
||||
self.frogpilot_stats["TotalLateralTime"] = self.total_lateral_engaged
|
||||
self.frogpilot_stats["TotalLongitudinalTime"] = self.total_longitudinal_engaged
|
||||
self.frogpilot_stats["TotalTrackedTime"] = self.total_tracked_time
|
||||
if sm["controlsState"].state != self.state:
|
||||
if sm["controlsState"].state == State.disabled:
|
||||
self.frogpilot_stats["Disengages"] = self.frogpilot_stats.get("Disengages", 0) + 1
|
||||
|
||||
params.put("FrogPilotStats", json.dumps(self.frogpilot_stats))
|
||||
if frogpilot_toggles.sound_pack == "frog":
|
||||
self.frogpilot_stats["FrogSqueaks"] = self.frogpilot_stats.get("FrogSqueaks", 0) + 1
|
||||
elif sm["controlsState"].state == State.enabled:
|
||||
self.frogpilot_stats["Engages"] = self.frogpilot_stats.get("Engages", 0) + 1
|
||||
|
||||
self.aol_engaged_time = 0
|
||||
self.drive_time = 0
|
||||
self.lateral_engaged_time = 0
|
||||
self.longitudinal_engaged_time = 0
|
||||
if frogpilot_toggles.sound_pack == "frog":
|
||||
self.frogpilot_stats["FrogChirps"] = self.frogpilot_stats.get("FrogChirps", 0) + 1
|
||||
elif sm["controlsState"].state == State.overriding:
|
||||
self.frogpilot_stats["Overrides"] = self.frogpilot_stats.get("Overrides", 0) + 1
|
||||
|
||||
self.state = sm["controlsState"].state
|
||||
|
||||
current_events = {event for event in self.frogpilot_events.event_names}
|
||||
if len(current_events) > 0:
|
||||
new_events = current_events - self.previous_events
|
||||
|
||||
if new_events:
|
||||
if (EventName.fcw in self.frogpilot_events.event_names or EventName.stockAeb in self.frogpilot_events.event_names):
|
||||
self.frogpilot_stats["AEBEvents"] = self.frogpilot_stats.get("AEBEvents", 0) + 1
|
||||
|
||||
self.previous_events = current_events
|
||||
|
||||
current_random_events = {event for event in self.frogpilot_events.events.names if event in RANDOM_EVENTS}
|
||||
if len(current_random_events) > 0:
|
||||
new_events = current_random_events - self.previous_random_events
|
||||
|
||||
if new_events:
|
||||
total_random_events = self.frogpilot_stats.get("RandomEvents", {})
|
||||
for event in new_events:
|
||||
event_name = RANDOM_EVENTS[event]
|
||||
total_random_events[event_name] = total_random_events.get(event_name, 0) + 1
|
||||
self.frogpilot_stats["RandomEvents"] = total_random_events
|
||||
|
||||
self.previous_random_events = current_random_events
|
||||
|
||||
if self.tracked_time > 60 and sm["carState"].standstill and self.enabled:
|
||||
if time_validated:
|
||||
current_month = now.month
|
||||
if current_month != self.frogpilot_stats.get("Month"):
|
||||
self.frogpilot_stats.update({
|
||||
"CurrentMonthsKilometers": 0,
|
||||
"Month": current_month
|
||||
})
|
||||
|
||||
self.frogpilot_stats["FrogPilotSeconds"] = self.frogpilot_stats.get("FrogPilotSeconds", 0) + self.tracked_time
|
||||
|
||||
self.frogpilot_stats["TrackedTime"] = self.frogpilot_stats.get("TrackedTime", 0) + self.tracked_time
|
||||
|
||||
current_model = self.model_name
|
||||
total_model_times = self.frogpilot_stats.get("ModelTimes", {})
|
||||
total_model_times[current_model] = total_model_times.get(current_model, 0) + self.tracked_time
|
||||
self.frogpilot_stats["ModelTimes"] = total_model_times
|
||||
|
||||
self.tracked_time = 0
|
||||
|
||||
if not self.drive_added:
|
||||
self.total_drives += 1
|
||||
params_tracking.put_int_nonblocking("FrogPilotDrives", self.total_drives)
|
||||
self.frogpilot_stats["FrogPilotDrives"] = self.frogpilot_stats.get("FrogPilotDrives", 0) + 1
|
||||
self.drive_added = True
|
||||
|
||||
params.put_nonblocking("FrogPilotStats", json.dumps(self.frogpilot_stats))
|
||||
|
||||
@@ -4,25 +4,22 @@ from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import COMFORT_BRAKE
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_variables import CRUISING_SPEED, PLANNER_TIME
|
||||
from openpilot.frogpilot.controls.lib.map_turn_speed_controller import MapTurnSpeedController
|
||||
from openpilot.frogpilot.controls.lib.curve_speed_controller import CurveSpeedController
|
||||
from openpilot.frogpilot.controls.lib.speed_limit_controller import SpeedLimitController
|
||||
|
||||
TARGET_LAT_A = 2.0
|
||||
|
||||
class FrogPilotVCruise:
|
||||
def __init__(self, FrogPilotPlanner):
|
||||
self.frogpilot_planner = FrogPilotPlanner
|
||||
|
||||
self.mtsc = MapTurnSpeedController()
|
||||
self.csc = CurveSpeedController(self)
|
||||
self.slc = SpeedLimitController()
|
||||
|
||||
self.forcing_stop = False
|
||||
self.override_force_stop = False
|
||||
|
||||
self.mtsc_target = 0
|
||||
self.override_force_stop_timer = 0
|
||||
|
||||
def update(self, gps_position, v_cruise, v_ego, sm, frogpilot_toggles):
|
||||
def update(self, gps_position, now, time_validated, v_cruise, v_ego, sm, frogpilot_toggles):
|
||||
force_stop = self.frogpilot_planner.cem.stop_light_detected and sm["controlsState"].enabled and frogpilot_toggles.force_stops
|
||||
force_stop &= self.frogpilot_planner.model_stopped
|
||||
force_stop &= self.override_force_stop_timer <= 0
|
||||
@@ -46,6 +43,21 @@ class FrogPilotVCruise:
|
||||
v_ego_cluster = max(sm["carState"].vEgoCluster, v_ego)
|
||||
v_ego_diff = v_ego_cluster - v_ego
|
||||
|
||||
# FrogsGoMoo's Curve Speed Controller
|
||||
if v_ego > CRUISING_SPEED and sm["controlsState"].enabled and self.frogpilot_planner.road_curvature_detected and frogpilot_toggles.curve_speed_controller:
|
||||
self.csc.update_target(v_ego)
|
||||
|
||||
self.csc_controlling_speed = True
|
||||
|
||||
self.csc_target = self.csc.target
|
||||
else:
|
||||
self.csc.log_data(v_ego, sm)
|
||||
|
||||
self.csc_controlling_speed = False
|
||||
self.csc.target_set = False
|
||||
|
||||
self.csc_target = v_cruise
|
||||
|
||||
# Mike's extended lead linear braking
|
||||
if self.frogpilot_planner.lead_one.vLead < v_ego > CRUISING_SPEED and sm["controlsState"].enabled and self.frogpilot_planner.tracking_lead and frogpilot_toggles.human_following:
|
||||
if not self.frogpilot_planner.frogpilot_following.following_lead:
|
||||
@@ -56,31 +68,17 @@ class FrogPilotVCruise:
|
||||
else:
|
||||
self.braking_target = v_cruise
|
||||
|
||||
# Pfeiferj's Map Turn Speed Controller
|
||||
if v_ego > CRUISING_SPEED and sm["controlsState"].enabled and frogpilot_toggles.map_turn_speed_controller:
|
||||
mtsc_active = self.mtsc_target < v_cruise
|
||||
|
||||
if self.frogpilot_planner.road_curvature_detected and mtsc_active:
|
||||
self.mtsc_target = self.mtsc_target
|
||||
elif not self.frogpilot_planner.road_curvature_detected and frogpilot_toggles.mtsc_curvature_check:
|
||||
self.mtsc_target = v_cruise
|
||||
else:
|
||||
mtsc_speed = ((TARGET_LAT_A * frogpilot_toggles.turn_aggressiveness) / (self.mtsc.get_map_curvature(gps_position, v_ego) * frogpilot_toggles.curve_sensitivity))**0.5
|
||||
self.mtsc_target = max(CRUISING_SPEED, mtsc_speed)
|
||||
else:
|
||||
self.mtsc_target = v_cruise
|
||||
|
||||
# Pfeiferj's Speed Limit Controller
|
||||
self.slc.frogpilot_toggles = frogpilot_toggles
|
||||
|
||||
if frogpilot_toggles.speed_limit_controller:
|
||||
self.slc.update_limits(sm["frogpilotCarState"].dashboardSpeedLimit, gps_position, sm["frogpilotNavigation"].navigationSpeedLimit, v_cruise, v_ego, sm)
|
||||
self.slc.update_limits(sm["frogpilotCarState"].dashboardSpeedLimit, gps_position, sm["frogpilotNavigation"].navigationSpeedLimit, now, time_validated, v_cruise, v_ego, sm)
|
||||
self.slc.update_override(v_cruise, v_cruise_diff, v_ego, v_ego_diff, sm)
|
||||
|
||||
self.slc_offset = self.slc.offset
|
||||
self.slc_target = self.slc.target
|
||||
elif frogpilot_toggles.show_speed_limits:
|
||||
self.slc.update_limits(sm["frogpilotCarState"].dashboardSpeedLimit, gps_position, sm["frogpilotNavigation"].navigationSpeedLimit, v_cruise, v_ego, sm)
|
||||
self.slc.update_limits(sm["frogpilotCarState"].dashboardSpeedLimit, gps_position, sm["frogpilotNavigation"].navigationSpeedLimit, now, time_validated, v_cruise, v_ego, sm)
|
||||
|
||||
self.slc_offset = 0
|
||||
self.slc_target = self.slc.target
|
||||
@@ -88,19 +86,7 @@ class FrogPilotVCruise:
|
||||
self.slc_offset = 0
|
||||
self.slc_target = 0
|
||||
|
||||
# Pfeiferj's Vision Turn Controller
|
||||
if v_ego > CRUISING_SPEED and sm["controlsState"].enabled and self.frogpilot_planner.road_curvature_detected and frogpilot_toggles.vision_turn_speed_controller:
|
||||
vtsc_speed = ((TARGET_LAT_A * frogpilot_toggles.turn_aggressiveness) / (abs(self.frogpilot_planner.road_curvature) * frogpilot_toggles.curve_sensitivity))**0.5
|
||||
self.vtsc_target = max(CRUISING_SPEED, vtsc_speed)
|
||||
else:
|
||||
self.vtsc_target = v_cruise
|
||||
|
||||
if sm["carState"].standstill and not self.override_force_stop and sm["controlsState"].enabled and frogpilot_toggles.force_standstill:
|
||||
self.forcing_stop = True
|
||||
|
||||
v_cruise = -1
|
||||
|
||||
elif force_stop_enabled and not self.override_force_stop:
|
||||
if force_stop_enabled and not self.override_force_stop:
|
||||
self.forcing_stop |= not sm["carState"].standstill
|
||||
|
||||
self.tracked_model_length = max(self.tracked_model_length - (v_ego * DT_MDL), 0)
|
||||
@@ -111,13 +97,10 @@ class FrogPilotVCruise:
|
||||
|
||||
self.tracked_model_length = self.frogpilot_planner.model_length
|
||||
|
||||
targets = [self.braking_target, self.mtsc_target, self.vtsc_target, v_cruise]
|
||||
targets = [self.braking_target, self.csc_target, v_cruise]
|
||||
if frogpilot_toggles.speed_limit_controller:
|
||||
targets.append(max(self.slc.overridden_speed, self.slc_target + self.slc_offset) - v_ego_diff)
|
||||
|
||||
v_cruise = min([target if target > CRUISING_SPEED else v_cruise for target in targets])
|
||||
|
||||
self.mtsc_target += v_cruise_diff
|
||||
self.vtsc_target += v_cruise_diff
|
||||
|
||||
return v_cruise
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# PFEIFER - MTSC - Modified by FrogAi for FrogPilot
|
||||
import json
|
||||
import math
|
||||
|
||||
from openpilot.common.conversions import Conversions as CV
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import calculate_distance_to_point
|
||||
from openpilot.frogpilot.common.frogpilot_variables import PLANNER_TIME, params_memory
|
||||
|
||||
def calculate_curvature(p1, p2, p3):
|
||||
lat1, lon1 = p1
|
||||
lat2, lon2 = p2
|
||||
lat3, lon3 = p3
|
||||
|
||||
lat1_rad, lon1_rad = lat1 * CV.DEG_TO_RAD, lon1 * CV.DEG_TO_RAD
|
||||
lat2_rad, lon2_rad = lat2 * CV.DEG_TO_RAD, lon2 * CV.DEG_TO_RAD
|
||||
lat3_rad, lon3_rad = lat3 * CV.DEG_TO_RAD, lon3 * CV.DEG_TO_RAD
|
||||
|
||||
side_a = calculate_distance_to_point(lat2_rad, lon2_rad, lat3_rad, lon3_rad)
|
||||
side_b = calculate_distance_to_point(lat1_rad, lon1_rad, lat3_rad, lon3_rad)
|
||||
side_c = calculate_distance_to_point(lat1_rad, lon1_rad, lat2_rad, lon2_rad)
|
||||
|
||||
s = (side_a + side_b + side_c) / 2
|
||||
|
||||
area_squared = s * (s - side_a) * (s - side_b) * (s - side_c)
|
||||
if area_squared <= 0:
|
||||
return 0
|
||||
|
||||
area = math.sqrt(area_squared)
|
||||
|
||||
radius = (side_a * side_b * side_c) / (4 * area)
|
||||
if radius == 0:
|
||||
return 0
|
||||
|
||||
curvature = 1 / radius
|
||||
return curvature
|
||||
|
||||
class MapTurnSpeedController:
|
||||
def get_map_curvature(self, gps_position, v_ego):
|
||||
if not gps_position:
|
||||
return 1e-6
|
||||
current_latitude = gps_position["latitude"]
|
||||
current_longitude = gps_position["longitude"]
|
||||
|
||||
distances = []
|
||||
minimum_idx = 0
|
||||
minimum_distance = 1000.0
|
||||
|
||||
target_velocities = json.loads(params_memory.get("MapTargetVelocities") or "[]")
|
||||
for i, target_velocity in enumerate(target_velocities):
|
||||
target_latitude = target_velocity["latitude"]
|
||||
target_longitude = target_velocity["longitude"]
|
||||
|
||||
distance = calculate_distance_to_point(current_latitude * CV.DEG_TO_RAD, current_longitude * CV.DEG_TO_RAD, target_latitude * CV.DEG_TO_RAD, target_longitude * CV.DEG_TO_RAD)
|
||||
distances.append(distance)
|
||||
|
||||
if distance < minimum_distance:
|
||||
minimum_distance = distance
|
||||
minimum_idx = i
|
||||
|
||||
forward_distances = distances[minimum_idx:]
|
||||
|
||||
cumulative_distance = 0.0
|
||||
target_idx = None
|
||||
|
||||
for i, distance in enumerate(forward_distances):
|
||||
cumulative_distance += distance
|
||||
if cumulative_distance >= PLANNER_TIME * v_ego:
|
||||
target_idx = i
|
||||
break
|
||||
|
||||
forward_points = target_velocities[minimum_idx:]
|
||||
|
||||
if target_idx is None or target_idx == 0 or target_idx >= len(forward_points) - 1:
|
||||
return 1e-6
|
||||
|
||||
p1 = (forward_points[target_idx - 1]["latitude"], forward_points[target_idx - 1]["longitude"])
|
||||
p2 = (forward_points[target_idx]["latitude"], forward_points[target_idx]["longitude"])
|
||||
p3 = (forward_points[target_idx + 1]["latitude"], forward_points[target_idx + 1]["longitude"])
|
||||
|
||||
return max(calculate_curvature(p1, p2, p3), 1e-6)
|
||||
@@ -2,6 +2,7 @@
|
||||
# Twilsonco's Lateral Neural Network Feedforward
|
||||
from collections import deque
|
||||
from difflib import SequenceMatcher
|
||||
from typing import NamedTuple
|
||||
|
||||
import json
|
||||
import math
|
||||
@@ -10,11 +11,10 @@ import os
|
||||
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.numpy_fast import interp
|
||||
from openpilot.selfdrive.car.interfaces import LatControlInputs
|
||||
from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_variables import TORQUE_NN_MODEL_PATH, get_nnff_model_files, params
|
||||
from openpilot.frogpilot.common.frogpilot_variables import NNFF_MODELS_PATH, get_nnff_model_files, params
|
||||
|
||||
# At higher speeds (25+mph) we can assume:
|
||||
# Lateral acceleration achieved by a specific car correlates to
|
||||
@@ -121,7 +121,7 @@ def get_nn_model_path(car, eps_firmware) -> str | None:
|
||||
return None, 0.0
|
||||
|
||||
best = max(candidates, key=lambda model: similarity(model, query))
|
||||
return os.path.join(TORQUE_NN_MODEL_PATH, f"{best}.json"), similarity(best, query)
|
||||
return os.path.join(NNFF_MODELS_PATH, f"{best}.json"), similarity(best, query)
|
||||
|
||||
def find_valid_model(*queries):
|
||||
for query in queries:
|
||||
@@ -152,13 +152,19 @@ def sign(x):
|
||||
def similarity(s1: str, s2: str) -> float:
|
||||
return SequenceMatcher(None, s1, s2).ratio()
|
||||
|
||||
class LatControlInputs(NamedTuple):
|
||||
lateral_acceleration: float
|
||||
roll_compensation: float
|
||||
vego: float
|
||||
aego: float
|
||||
|
||||
class NeuralNetworkFeedforward:
|
||||
def __init__(self, CP, CI, LatControlTorque):
|
||||
def __init__(self, CP, LatControlTorque):
|
||||
self.lat_control_torque = LatControlTorque
|
||||
|
||||
self.lat_torque_nn_model = get_nn_model(CP.carFingerprint, str(next((fw.fwVersion for fw in CP.carFw if fw.ecu == "eps"), "")).replace("\\", ""))
|
||||
|
||||
self.torque_from_lateral_accel = CI.torque_from_lateral_accel()
|
||||
self.torque_from_lateral_accel = self.lat_control_torque.torque_from_lateral_accel
|
||||
|
||||
self.use_steering_angle = self.lat_control_torque.torque_params.useSteeringAngle
|
||||
|
||||
@@ -200,8 +206,6 @@ class NeuralNetworkFeedforward:
|
||||
self.past_future_len = len(self.past_times) + len(self.nn_future_times)
|
||||
self.roll_deque = deque(maxlen=history_check_frames[0])
|
||||
|
||||
self.nnLog = []
|
||||
|
||||
def update_live_delay(self, lateral_delay):
|
||||
self.lateral_delay = lateral_delay
|
||||
|
||||
@@ -285,25 +289,15 @@ class NeuralNetworkFeedforward:
|
||||
|
||||
# apply friction override for cars with low NN friction response
|
||||
if self.nn_friction_override:
|
||||
pid_log.error += self.torque_from_lateral_accel(LatControlInputs(0.0, 0.0, CS.vEgo, CS.aEgo), self.lat_control_torque.torque_params,
|
||||
friction_input, lateral_accel_deadzone, friction_compensation=True, gravity_adjusted=False)
|
||||
|
||||
self.nnLog = nn_input + nnff_setpoint_input + nnff_measurement_input
|
||||
pid_log.error += self.torque_from_lateral_accel(0.0, self.lat_control_torque.torque_params)
|
||||
else:
|
||||
torque_from_setpoint = self.torque_from_lateral_accel(LatControlInputs(setpoint, roll_compensation, CS.vEgo, CS.aEgo), self.lat_control_torque.torque_params,
|
||||
lateral_jerk_setpoint, lateral_accel_deadzone, friction_compensation=True, gravity_adjusted=False)
|
||||
torque_from_measurement = self.torque_from_lateral_accel(measurement, self.lat_control_torque.torque_params)
|
||||
torque_from_setpoint = self.torque_from_lateral_accel(setpoint, self.lat_control_torque.torque_params)
|
||||
|
||||
torque_from_measurement = self.torque_from_lateral_accel(LatControlInputs(measurement, roll_compensation, CS.vEgo, CS.aEgo), self.lat_control_torque.torque_params,
|
||||
lateral_jerk_measurement, lateral_accel_deadzone, friction_compensation=True, gravity_adjusted=False)
|
||||
|
||||
pid_log.error = torque_from_setpoint - torque_from_measurement
|
||||
pid_log.error = float(torque_from_setpoint - torque_from_measurement)
|
||||
|
||||
error = desired_lateral_accel - actual_lateral_accel
|
||||
friction_input = self.lat_accel_friction_factor * error + self.lat_jerk_friction_factor * lookahead_lateral_jerk
|
||||
ff = self.torque_from_lateral_accel(LatControlInputs(gravity_adjusted_lateral_accel, roll_compensation, CS.vEgo, CS.aEgo), self.lat_control_torque.torque_params,
|
||||
friction_input, lateral_accel_deadzone, friction_compensation=True,
|
||||
gravity_adjusted=True)
|
||||
ff = self.torque_from_lateral_accel(gravity_adjusted_lateral_accel, self.lat_control_torque.torque_params)
|
||||
|
||||
self.nnLog = []
|
||||
|
||||
return torque_from_setpoint, torque_from_measurement, pid_log, ff
|
||||
return pid_log, ff
|
||||
|
||||
@@ -7,11 +7,9 @@ import numpy as np
|
||||
import requests
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import datetime
|
||||
|
||||
from openpilot.common.conversions import Conversions as CV
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.common.time import system_time_valid
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import calculate_bearing_offset, calculate_distance_to_point, is_url_pingable
|
||||
from openpilot.frogpilot.common.frogpilot_variables import params, params_memory
|
||||
@@ -37,7 +35,6 @@ class SpeedLimitController:
|
||||
self.source = "None"
|
||||
|
||||
self.mapbox_requests = json.loads(params.get("MapBoxRequests") or "{}")
|
||||
self.mapbox_requests.setdefault("month", datetime.now().month)
|
||||
self.mapbox_requests.setdefault("total_requests", 0)
|
||||
self.mapbox_requests.setdefault("max_requests", FREE_MAPBOX_REQUESTS - (28 * 100))
|
||||
|
||||
@@ -80,7 +77,7 @@ class SpeedLimitController:
|
||||
]
|
||||
return next((offset for low, high, offset in offset_map if low < self.target < high), 0)
|
||||
|
||||
def get_mapbox_speed_limit(self, gps_position, v_ego, sm):
|
||||
def get_mapbox_speed_limit(self, gps_position, now, time_validated, v_ego, sm):
|
||||
if not gps_position or not self.mapbox_token or (sm["carState"].steeringAngleDeg - sm["liveParameters"].angleOffsetDeg) >= 45:
|
||||
self.mapbox_limit = 0
|
||||
self.segment_distance = 0
|
||||
@@ -107,13 +104,13 @@ class SpeedLimitController:
|
||||
self.segment_distance = 1000
|
||||
return None
|
||||
|
||||
if system_time_valid():
|
||||
current_month = datetime.now().month
|
||||
if time_validated:
|
||||
current_month = now.month
|
||||
if current_month != self.mapbox_requests.get("month"):
|
||||
self.mapbox_requests.update({
|
||||
"month": current_month,
|
||||
"total_requests": 0,
|
||||
"max_requests": FREE_MAPBOX_REQUESTS - calendar.monthrange(datetime.now().year, current_month)[1] * 100,
|
||||
"max_requests": FREE_MAPBOX_REQUESTS - calendar.monthrange(now.year, current_month)[1] * 100,
|
||||
})
|
||||
|
||||
self.mapbox_requests["total_requests"] += 1
|
||||
@@ -204,7 +201,7 @@ class SpeedLimitController:
|
||||
def handle_limit_change(self, desired_source, desired_target, sm):
|
||||
self.speed_limit_changed_timer += DT_MDL
|
||||
|
||||
speed_limit_accepted = (sm["frogpilotCarState"].accelPressed and not sm["carControl"].cruiseControl.override) or params_memory.get_bool("SpeedLimitAccepted")
|
||||
speed_limit_accepted = (sm["frogpilotCarState"].accelPressed and sm["carControl"].longActive) or params_memory.get_bool("SpeedLimitAccepted")
|
||||
speed_limit_denied = sm["frogpilotCarState"].decelPressed or (self.speed_limit_changed_timer >= 30)
|
||||
|
||||
if speed_limit_accepted:
|
||||
@@ -241,7 +238,7 @@ class SpeedLimitController:
|
||||
|
||||
params.put_float_nonblocking("PreviousSpeedLimit", self.target)
|
||||
|
||||
def update_limits(self, dashboard_speed_limit, gps_position, navigation_speed_limit, v_cruise, v_ego, sm):
|
||||
def update_limits(self, dashboard_speed_limit, gps_position, navigation_speed_limit, now, time_validated, v_cruise, v_ego, sm):
|
||||
self.update_map_speed_limit(gps_position, v_ego)
|
||||
|
||||
limits = {
|
||||
@@ -279,7 +276,7 @@ class SpeedLimitController:
|
||||
|
||||
if desired_target == 0 or self.target == 0:
|
||||
if self.mapbox_requests["total_requests"] < self.mapbox_requests["max_requests"] and self.frogpilot_toggles.slc_mapbox_filler:
|
||||
self.get_mapbox_speed_limit(gps_position, v_ego, sm)
|
||||
self.get_mapbox_speed_limit(gps_position, now, time_validated, v_ego, sm)
|
||||
|
||||
if self.mapbox_limit >= 1:
|
||||
desired_source = "Mapbox"
|
||||
@@ -307,23 +304,6 @@ class SpeedLimitController:
|
||||
self.speed_limit_changed_timer = 0
|
||||
self.unconfirmed_speed_limit = 0
|
||||
|
||||
def update_override(self, v_cruise, v_cruise_diff, v_ego, v_ego_diff, sm):
|
||||
self.override_slc = self.overridden_speed > self.target + self.offset > 0
|
||||
self.override_slc |= sm["carState"].gasPressed and v_ego > self.target + self.offset > 0
|
||||
self.override_slc &= sm["controlsState"].enabled
|
||||
|
||||
if self.override_slc:
|
||||
if self.frogpilot_toggles.speed_limit_controller_override_manual:
|
||||
if sm["carState"].gasPressed:
|
||||
self.overridden_speed = max(v_ego + v_ego_diff, self.overridden_speed)
|
||||
self.overridden_speed = float(np.clip(self.overridden_speed, self.target + self.offset, v_cruise + v_cruise_diff))
|
||||
elif self.frogpilot_toggles.speed_limit_controller_override_set_speed:
|
||||
self.overridden_speed = v_cruise + v_cruise_diff
|
||||
|
||||
self.source = "None"
|
||||
else:
|
||||
self.overridden_speed = 0
|
||||
|
||||
def update_map_speed_limit(self, gps_position, v_ego):
|
||||
if not gps_position:
|
||||
return
|
||||
@@ -351,3 +331,20 @@ class SpeedLimitController:
|
||||
|
||||
if distance_to_upcoming < max_lookahead:
|
||||
self.map_speed_limit = self.next_speed_limit
|
||||
|
||||
def update_override(self, v_cruise, v_cruise_diff, v_ego, v_ego_diff, sm):
|
||||
self.override_slc = self.overridden_speed > self.target + self.offset > 0
|
||||
self.override_slc |= sm["carState"].gasPressed and v_ego > self.target + self.offset > 0
|
||||
self.override_slc &= sm["controlsState"].enabled
|
||||
|
||||
if self.override_slc:
|
||||
if self.frogpilot_toggles.speed_limit_controller_override_manual:
|
||||
if sm["carState"].gasPressed:
|
||||
self.overridden_speed = max(v_ego + v_ego_diff, self.overridden_speed)
|
||||
self.overridden_speed = float(np.clip(self.overridden_speed, self.target + self.offset, v_cruise + v_cruise_diff))
|
||||
elif self.frogpilot_toggles.speed_limit_controller_override_set_speed:
|
||||
self.overridden_speed = v_cruise + v_cruise_diff
|
||||
|
||||
self.source = "None"
|
||||
else:
|
||||
self.overridden_speed = 0
|
||||
|
||||
@@ -10,8 +10,8 @@ from cereal import messaging
|
||||
from openpilot.common.realtime import DT_MDL, Priority, Ratekeeper, config_realtime_process
|
||||
from openpilot.common.time import system_time_valid
|
||||
|
||||
from openpilot.frogpilot.assets.model_manager import ModelManager, MODEL_DOWNLOAD_ALL_PARAM, MODEL_DOWNLOAD_PARAM
|
||||
from openpilot.frogpilot.assets.theme_manager import ThemeManager
|
||||
from openpilot.frogpilot.assets.model_manager import MODEL_DOWNLOAD_ALL_PARAM, MODEL_DOWNLOAD_PARAM, ModelManager
|
||||
from openpilot.frogpilot.assets.theme_manager import THEME_COMPONENT_PARAMS, ThemeManager
|
||||
from openpilot.frogpilot.common.frogpilot_functions import backup_toggles
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import flash_panda, is_url_pingable, lock_doors, run_thread_with_lock, update_maps, update_openpilot
|
||||
from openpilot.frogpilot.common.frogpilot_variables import ERROR_LOGS_PATH, FrogPilotVariables, get_frogpilot_toggles, params, params_cache, params_memory
|
||||
@@ -21,9 +21,11 @@ from openpilot.frogpilot.system.frogpilot_stats import send_stats
|
||||
|
||||
ASSET_CHECK_RATE = (1 / DT_MDL)
|
||||
|
||||
def assets_checks(model_manager, theme_manager):
|
||||
def assets_checks(model_manager, theme_manager, frogpilot_toggles):
|
||||
if params_memory.get_bool(MODEL_DOWNLOAD_ALL_PARAM):
|
||||
run_thread_with_lock("download_all_models", model_manager.download_all_models)
|
||||
elif params_memory.get_bool("UpdateTinygrad"):
|
||||
run_thread_with_lock("update_tinygrad", model_manager.update_tinygrad)
|
||||
else:
|
||||
model_to_download = params_memory.get(MODEL_DOWNLOAD_PARAM, encoding="utf-8")
|
||||
if model_to_download:
|
||||
@@ -34,24 +36,15 @@ def assets_checks(model_manager, theme_manager):
|
||||
|
||||
report_data = json.loads(params_memory.get("IssueReported", encoding="utf-8") or "{}")
|
||||
if report_data:
|
||||
sentry.capture_report(report_data["DiscordUser"], report_data["Issue"], vars(get_frogpilot_toggles()))
|
||||
sentry.capture_report(report_data["DiscordUser"], report_data["Issue"], vars(frogpilot_toggles))
|
||||
params_memory.remove("IssueReported")
|
||||
|
||||
assets = [
|
||||
("ColorToDownload", "colors"),
|
||||
("DistanceIconToDownload", "distance_icons"),
|
||||
("IconToDownload", "icons"),
|
||||
("SignalToDownload", "signals"),
|
||||
("SoundToDownload", "sounds"),
|
||||
("WheelToDownload", "steering_wheels")
|
||||
]
|
||||
|
||||
for param, asset_type in assets:
|
||||
asset_to_download = params_memory.get(param, encoding="utf-8")
|
||||
for asset_type, asset_param in THEME_COMPONENT_PARAMS.items():
|
||||
asset_to_download = params_memory.get(asset_param, encoding="utf-8")
|
||||
if asset_to_download:
|
||||
run_thread_with_lock("download_theme", theme_manager.download_theme, (asset_type, asset_to_download, param))
|
||||
run_thread_with_lock("download_theme", theme_manager.download_theme, (asset_type, asset_to_download, asset_param, frogpilot_toggles))
|
||||
|
||||
def update_checks(manually_updated, model_manager, now, theme_manager, frogpilot_toggles, boot_run=False):
|
||||
def update_checks(model_manager, now, theme_manager, frogpilot_toggles, boot_run=False):
|
||||
while not (is_url_pingable("https://github.com") or is_url_pingable("https://gitlab.com")):
|
||||
time.sleep(60)
|
||||
|
||||
@@ -60,7 +53,7 @@ def update_checks(manually_updated, model_manager, now, theme_manager, frogpilot
|
||||
|
||||
run_thread_with_lock("update_maps", update_maps, (now,))
|
||||
|
||||
if frogpilot_toggles.automatic_updates and not manually_updated:
|
||||
if frogpilot_toggles.automatic_updates:
|
||||
run_thread_with_lock("update_openpilot", update_openpilot)
|
||||
|
||||
time.sleep(1)
|
||||
@@ -80,14 +73,14 @@ def frogpilot_thread():
|
||||
model_manager = ModelManager()
|
||||
theme_manager = ThemeManager()
|
||||
|
||||
toggles_last_updated = datetime.datetime.now()
|
||||
toggles_last_updated = datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
pm = messaging.PubMaster(["frogpilotPlan"])
|
||||
sm = messaging.SubMaster(["carControl", "carState", "controlsState", "deviceState", "driverMonitoringState",
|
||||
"liveLocationKalman", "liveParameters", "managerState", "modelV2",
|
||||
"pandaStates", "radarState", "frogpilotCarState",
|
||||
"frogpilotNavigation"],
|
||||
poll="modelV2", ignore_avg_freq=["radarState"])
|
||||
"liveLocationKalman", "liveParameters", "managerState", "modelV2", "onroadEvents",
|
||||
"pandaStates", "radarState", "frogpilotCarState", "frogpilotControlsState",
|
||||
"frogpilotModelV2", "frogpilotNavigation", "frogpilotOnroadEvents"],
|
||||
poll="modelV2", ignore_avg_freq=["frogpilotRadarState"])
|
||||
|
||||
run_update_checks = False
|
||||
started_previously = False
|
||||
@@ -97,7 +90,7 @@ def frogpilot_thread():
|
||||
while True:
|
||||
sm.update()
|
||||
|
||||
now = datetime.datetime.now()
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
started = sm["deviceState"].started
|
||||
|
||||
@@ -116,32 +109,28 @@ def frogpilot_thread():
|
||||
if time_validated and is_url_pingable(os.environ.get("STATS_URL", "")):
|
||||
send_stats()
|
||||
|
||||
params_memory.put_bool("IsOnroad", False)
|
||||
|
||||
elif started and not started_previously:
|
||||
frogpilot_planner = FrogPilotPlanner()
|
||||
frogpilot_tracking = FrogPilotTracking()
|
||||
|
||||
if error_log.is_file():
|
||||
error_log.unlink()
|
||||
|
||||
params_memory.put_bool("IsOnroad", True)
|
||||
frogpilot_planner = FrogPilotPlanner(error_log, theme_manager)
|
||||
frogpilot_tracking = FrogPilotTracking(frogpilot_planner, frogpilot_toggles)
|
||||
|
||||
if started and sm.updated["modelV2"]:
|
||||
frogpilot_planner.update(sm, frogpilot_toggles)
|
||||
frogpilot_planner.publish(sm, pm, theme_manager.theme_updated, toggles_updated)
|
||||
frogpilot_planner.update(now, time_validated, sm, frogpilot_toggles)
|
||||
frogpilot_planner.publish(theme_manager.theme_updated, toggles_updated, sm, pm, frogpilot_toggles)
|
||||
|
||||
frogpilot_tracking.update(sm)
|
||||
elif not started and toggles_updated:
|
||||
frogpilot_tracking.update(now, time_validated, sm, frogpilot_toggles)
|
||||
elif not started:
|
||||
frogpilot_plan_send = messaging.new_message("frogpilotPlan")
|
||||
frogpilot_plan_send.frogpilotPlan.themeUpdated = theme_manager.theme_updated
|
||||
frogpilot_plan_send.frogpilotPlan.themeUpdated = theme_manager.theme_updated or params_memory.get_bool("UseActiveTheme")
|
||||
frogpilot_plan_send.frogpilotPlan.togglesUpdated = toggles_updated
|
||||
pm.send("frogpilotPlan", frogpilot_plan_send)
|
||||
|
||||
started_previously = started
|
||||
|
||||
if rate_keeper.frame % ASSET_CHECK_RATE == 0:
|
||||
assets_checks(model_manager, theme_manager)
|
||||
assets_checks(model_manager, theme_manager, frogpilot_toggles)
|
||||
|
||||
if params_memory.get_bool("FrogPilotTogglesUpdated") or theme_manager.theme_updated:
|
||||
previous_holiday_themes = frogpilot_toggles.holiday_themes
|
||||
@@ -163,15 +152,13 @@ def frogpilot_thread():
|
||||
|
||||
toggles_updated = (now - toggles_last_updated).total_seconds() <= 1
|
||||
|
||||
manually_updated = params_memory.get_bool("ManualUpdateInitiated")
|
||||
|
||||
run_update_checks |= manually_updated
|
||||
run_update_checks |= params_memory.get_bool("ManualUpdateInitiated")
|
||||
run_update_checks |= now.second == 0 and (now.minute % 60 == 0 or (now.minute % 5 == 0 and frogpilot_toggles.frogs_go_moo))
|
||||
run_update_checks &= time_validated
|
||||
|
||||
if run_update_checks:
|
||||
theme_manager.update_active_theme(time_validated, frogpilot_toggles)
|
||||
run_thread_with_lock("update_checks", update_checks, (manually_updated, model_manager, now, theme_manager, frogpilot_toggles))
|
||||
run_thread_with_lock("update_checks", update_checks, (model_manager, now, theme_manager, frogpilot_toggles))
|
||||
|
||||
run_update_checks = False
|
||||
elif not time_validated:
|
||||
@@ -180,7 +167,7 @@ def frogpilot_thread():
|
||||
continue
|
||||
|
||||
theme_manager.update_active_theme(time_validated, frogpilot_toggles)
|
||||
run_thread_with_lock("update_checks", update_checks, (manually_updated, model_manager, now, theme_manager, frogpilot_toggles, True))
|
||||
run_thread_with_lock("update_checks", update_checks, (model_manager, now, theme_manager, frogpilot_toggles, True))
|
||||
|
||||
rate_keeper.keep_time()
|
||||
|
||||
|
||||
@@ -2,18 +2,21 @@ import json
|
||||
import os
|
||||
import random
|
||||
import requests
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "third_party"))
|
||||
|
||||
from collections import Counter
|
||||
from datetime import datetime, timezone
|
||||
from influxdb_client import InfluxDBClient, Point
|
||||
from influxdb_client.client.write_api import SYNCHRONOUS
|
||||
|
||||
from openpilot.common.conversions import Conversions as CV
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.system.version import get_build_metadata
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import run_cmd
|
||||
from openpilot.frogpilot.common.frogpilot_variables import get_frogpilot_toggles, params, params_tracking
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import clean_model_name, run_cmd
|
||||
from openpilot.frogpilot.common.frogpilot_variables import get_frogpilot_toggles, params
|
||||
|
||||
BASE_URL = "https://nominatim.openstreetmap.org"
|
||||
MINIMUM_POPULATION = 100_000
|
||||
@@ -94,23 +97,11 @@ def get_city_center(latitude, longitude):
|
||||
print(f"Falling back to (0, 0) for {latitude}, {longitude}")
|
||||
return float(0.0), float(0.0), "N/A", "N/A", "N/A"
|
||||
|
||||
def install_influxdb_client():
|
||||
try:
|
||||
import influxdb_client
|
||||
import influxdb_client.client.write_api
|
||||
except ModuleNotFoundError:
|
||||
print("influxdb-client not found. Attempting installation...")
|
||||
stock_mount_options = subprocess.run(["findmnt", "-no", "OPTIONS", "/"], capture_output=True, text=True, check=True).stdout.strip()
|
||||
|
||||
run_cmd(["sudo", "mount", "-o", "remount,rw", "/"], "Successfully remounted / as read-write", "Failed to remount / as read-write", report=False)
|
||||
run_cmd(["sudo", sys.executable, "-m", "pip", "install", "influxdb-client"], "Successfully installed influxdb-client", "Failed to install influxdb-client", report=False)
|
||||
run_cmd(["sudo", "mount", "-o", f"remount,{stock_mount_options}", "/"], "Successfully restored stock mount options", "Failed to restore stock mount options", report=False)
|
||||
|
||||
def is_up_to_date(build_metadata):
|
||||
remote_commit = subprocess.check_output(["git", "ls-remote", "origin", build_metadata.channel], text=True, stderr=subprocess.DEVNULL).strip()
|
||||
remote_commit = run_cmd(["git", "ls-remote", "origin", build_metadata.channel], f"Fetched remote commit", "Failed to fetch remote commit", report=False)
|
||||
|
||||
if remote_commit:
|
||||
return build_metadata.openpilot.git_commit == remote_commit.split()[0]
|
||||
return build_metadata.openpilot.git_commit == remote_commit.strip().split()[0]
|
||||
|
||||
return True
|
||||
|
||||
@@ -125,11 +116,6 @@ def send_stats():
|
||||
if frogpilot_toggles.car_make == "mock":
|
||||
return
|
||||
|
||||
install_influxdb_client()
|
||||
|
||||
from influxdb_client import InfluxDBClient, Point
|
||||
from influxdb_client.client.write_api import SYNCHRONOUS
|
||||
|
||||
bucket = os.environ.get("STATS_BUCKET", "")
|
||||
org_ID = os.environ.get("STATS_ORG_ID", "")
|
||||
token = os.environ.get("STATS_TOKEN", "")
|
||||
@@ -159,16 +145,18 @@ def send_stats():
|
||||
selected_theme = random.choice([item for item, count in most_common if count == max_count]).replace("-user_created", "").replace("_", " ")
|
||||
|
||||
point = (Point("user_stats")
|
||||
.field("blocked_user", frogpilot_toggles.block_user)
|
||||
.field("car_make", "GM" if frogpilot_toggles.car_make == "gm" else frogpilot_toggles.car_make.title())
|
||||
.field("car_model", frogpilot_toggles.car_model)
|
||||
.field("city", city)
|
||||
.field("country", country)
|
||||
.field("current_months_kilometers", int(frogpilot_stats.get("CurrentMonthsKilometers", 0)))
|
||||
.field("device", HARDWARE.get_device_type())
|
||||
.field("driving_model", frogpilot_toggles.model_name.replace("🗺️", "").replace("📡", "").replace("👀", "").replace("(Default)", "").strip())
|
||||
.field("driving_model", clean_model_name(frogpilot_toggles.model_name))
|
||||
.field("event", 1)
|
||||
.field("frogpilot_drives", params_tracking.get_int("FrogPilotDrives"))
|
||||
.field("frogpilot_hours", params_tracking.get_int("FrogPilotMinutes") / 60)
|
||||
.field("frogpilot_miles", params_tracking.get_int("FrogPilotKilometers") * CV.KPH_TO_MPH)
|
||||
.field("frogpilot_drives", int(frogpilot_stats.get("FrogPilotDrives", 0)))
|
||||
.field("frogpilot_hours", float(frogpilot_stats.get("FrogPilotSeconds", 0)) / (60 * 60))
|
||||
.field("frogpilot_miles", float(frogpilot_stats.get("FrogPilotMeters", 0)) * CV.METER_TO_MILE)
|
||||
.field("goat_scream", frogpilot_toggles.goat_scream_alert)
|
||||
.field("has_cc_long", frogpilot_toggles.has_cc_long)
|
||||
.field("has_openpilot_longitudinal", frogpilot_toggles.openpilot_longitudinal)
|
||||
@@ -181,10 +169,10 @@ def send_stats():
|
||||
.field("random_events", frogpilot_toggles.random_events)
|
||||
.field("state", state)
|
||||
.field("theme", selected_theme.title())
|
||||
.field("total_aol_seconds", float(frogpilot_stats.get("TotalAOLTime", 0)))
|
||||
.field("total_lateral_seconds", float(frogpilot_stats.get("TotalLateralTime", 0)))
|
||||
.field("total_longitudinal_seconds", float(frogpilot_stats.get("TotalLongitudinalTime", 0)))
|
||||
.field("total_tracked_seconds", float(frogpilot_stats.get("TotalTrackedTime", 0)))
|
||||
.field("total_aol_seconds", float(frogpilot_stats.get("AOLTime", 0)))
|
||||
.field("total_lateral_seconds", float(frogpilot_stats.get("LateralTime", 0)))
|
||||
.field("total_longitudinal_seconds", float(frogpilot_stats.get("LongitudinalTime", 0)))
|
||||
.field("total_tracked_seconds", float(frogpilot_stats.get("TrackedTime", 0)))
|
||||
.field("tuning_level", params.get_int("TuningLevel") + 1 if params.get_bool("TuningLevelConfirmed") else 0)
|
||||
.field("up_to_date", is_up_to_date(build_metadata))
|
||||
.field("using_stock_acc", not (frogpilot_toggles.has_cc_long or frogpilot_toggles.openpilot_longitudinal))
|
||||
|
||||
@@ -7,8 +7,6 @@ import time
|
||||
from collections import OrderedDict, deque
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import openpilot.system.sentry as sentry
|
||||
|
||||
from cereal import log, messaging
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import calculate_distance_to_point, calculate_lane_width, is_url_pingable
|
||||
@@ -126,7 +124,7 @@ class MapSpeedLogger:
|
||||
if self.should_stop_processing:
|
||||
return False
|
||||
|
||||
time.sleep(10)
|
||||
time.sleep(5)
|
||||
return True
|
||||
|
||||
def fetch_from_overpass(self, latitude, longitude):
|
||||
@@ -379,35 +377,29 @@ def main():
|
||||
previously_started = False
|
||||
|
||||
while True:
|
||||
try:
|
||||
logger.sm.update()
|
||||
logger.sm.update()
|
||||
|
||||
if logger.sm["deviceState"].started:
|
||||
logger.log_speed_limit()
|
||||
if logger.sm["deviceState"].started:
|
||||
logger.log_speed_limit()
|
||||
|
||||
previously_started = True
|
||||
elif previously_started:
|
||||
existing_dataset = json.loads(params.get("SpeedLimits") or "[]")
|
||||
existing_dataset.extend(logger.dataset_additions)
|
||||
previously_started = True
|
||||
elif previously_started:
|
||||
existing_dataset = json.loads(params.get("SpeedLimits") or "[]")
|
||||
existing_dataset.extend(logger.dataset_additions)
|
||||
|
||||
new_dataset = logger.cleanup_dataset(existing_dataset)
|
||||
params.put("SpeedLimits", json.dumps(list(new_dataset)))
|
||||
new_dataset = logger.cleanup_dataset(existing_dataset)
|
||||
params.put("SpeedLimits", json.dumps(list(new_dataset)))
|
||||
|
||||
if logger.sm["deviceState"].networkType in (NetworkType.ethernet, NetworkType.wifi):
|
||||
params_memory.put_bool("UpdateSpeedLimits", True)
|
||||
if logger.sm["deviceState"].networkType in (NetworkType.ethernet, NetworkType.wifi):
|
||||
params_memory.put_bool("UpdateSpeedLimits", True)
|
||||
|
||||
logger.dataset_additions.clear()
|
||||
logger.dataset_additions.clear()
|
||||
|
||||
previously_started = False
|
||||
elif params_memory.get_bool("UpdateSpeedLimits"):
|
||||
logger.process_speed_limits()
|
||||
else:
|
||||
time.sleep(5)
|
||||
|
||||
except Exception as exception:
|
||||
print(f"Error in speed_limit_filler: {exception}")
|
||||
sentry.capture_exception(exception)
|
||||
time.sleep(1)
|
||||
previously_started = False
|
||||
elif params_memory.get_bool("UpdateSpeedLimits"):
|
||||
logger.process_speed_limits()
|
||||
else:
|
||||
time.sleep(5)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -782,7 +782,7 @@ function NavigationDestination({
|
||||
if (fav) {
|
||||
removeFavorite(fav);
|
||||
} else {
|
||||
showSnackbar("Couldn’t find favorite entry…");
|
||||
showSnackbar("Couldn't find favorite entry…");
|
||||
}
|
||||
} else {
|
||||
await favoriteDestination();
|
||||
|
||||
@@ -227,7 +227,7 @@ async function openOverlay(route) {
|
||||
|
||||
downloadButton.onclick = () => {
|
||||
const link = document.createElement("a");
|
||||
const videoPath = `/video/${route.name}--${current}?camera=${selectedCamera}`;
|
||||
const videoPath = `/video/${route.name}/combined?camera=${selectedCamera}`;
|
||||
link.href = videoPath;
|
||||
link.download = `${route.timestamp}-${selectedCamera}.mp4`;
|
||||
document.body.appendChild(link);
|
||||
@@ -251,8 +251,7 @@ async function openOverlay(route) {
|
||||
vid.load();
|
||||
vid.play();
|
||||
} catch (error) {
|
||||
showSnackbar("Error: Could not load all route segments.", "error");
|
||||
segments = [`/video/${route.name}--0`];
|
||||
showSnackbar("Error: Could not load combined route video.", "error");
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -271,8 +270,7 @@ async function openOverlay(route) {
|
||||
overlay.querySelectorAll(".camera-button").forEach(btn => btn.classList.remove("active"));
|
||||
e.target.classList.add("active");
|
||||
selectedCamera = e.target.dataset.camera;
|
||||
const videoPath = segments[current].includes("?") ? `${segments[current]}&camera=${selectedCamera}` : `${segments[current]}?camera=${selectedCamera}`
|
||||
vid.src = videoPath;
|
||||
vid.src = segments[current].includes("?") ? `${segments[current]}&camera=${selectedCamera}` : `${segments[current]}?camera=${selectedCamera}`;
|
||||
vid.load();
|
||||
vid.play();
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import { ScreenRecordings } from "/assets/components/recordings/screen_recording
|
||||
import { Sidebar } from "/assets/components/sidebar.js"
|
||||
import { SpeedLimits } from "/assets/components/tools/speed_limits.js"
|
||||
import { TailscaleControl } from "/assets/components/tailscale/tailscale.js"
|
||||
import { ThemeMaker } from "/assets/components/tools/theme_maker.js"
|
||||
import { TmuxLog } from "/assets/components/tools/tmux.js"
|
||||
import { ToggleControl } from "/assets/components/tools/toggles.js"
|
||||
import { TSKManager } from "/assets/components/tools/tsk_manager.js"
|
||||
@@ -39,6 +40,7 @@ function Root() {
|
||||
createRoute("settings", "/settings/:section/:subsection?", SettingsView),
|
||||
createRoute("speed_limits", "/download_speed_limits", SpeedLimits),
|
||||
createRoute("tailscale", "/manage_tailscale", TailscaleControl),
|
||||
createRoute("thememaker", "/theme_maker", ThemeMaker),
|
||||
createRoute("tmux", "/manage_tmux", TmuxLog),
|
||||
createRoute("toggles", "/manage_toggles", ToggleControl),
|
||||
createRoute("tsk_manager", "/tsk_manager", TSKManager),
|
||||
|
||||
@@ -21,6 +21,7 @@ const MenuItems = {
|
||||
{ name: "Download Speed Limits", link: "/download_speed_limits", icon: "bi-download" },
|
||||
{ name: "Error Logs", link: "/manage_error_logs", icon: "bi-exclamation-triangle" },
|
||||
{ name: "Lock/Unlock Doors", link: "/lock_or_unlock_doors", icon: "bi-door-closed" },
|
||||
{ name: "Theme Maker", link: "/theme_maker", icon: "bi-palette-fill" },
|
||||
{ name: "Tmux Log", link: "/manage_tmux", icon: "bi-terminal" },
|
||||
{ name: "Toggles", link: "/manage_toggles", icon: "bi-toggle-on" },
|
||||
{ name: "Toyota Security Keys", link: "/tsk_manager", icon: "bi-key-fill" },
|
||||
|
||||
@@ -0,0 +1,720 @@
|
||||
.apply-button {
|
||||
background-color: var(--accent-bg);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-lg);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-bold);
|
||||
overflow: hidden;
|
||||
padding: 0.75rem 1.5rem;
|
||||
transition: background-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.apply-button:hover {
|
||||
background-color: var(--accent-hover-bg);
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.checklist-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-sm);
|
||||
margin-top: var(--margin-base);
|
||||
}
|
||||
|
||||
.checklist-item {
|
||||
align-items: center;
|
||||
background-color: var(--input-bg);
|
||||
border-radius: var(--border-radius-md);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
padding: var(--padding-sm);
|
||||
transition: background-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.checklist-item .custom-checkbox {
|
||||
background-color: var(--color-gray-600);
|
||||
border-radius: 13px;
|
||||
flex-shrink: 0;
|
||||
height: 26px;
|
||||
position: relative;
|
||||
transition: background-color var(--transition-fast);
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.checklist-item .custom-checkbox::before {
|
||||
background-color: var(--color-white);
|
||||
border-radius: 50%;
|
||||
content: "";
|
||||
height: 20px;
|
||||
left: 4px;
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
transition: transform var(--transition-fast);
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.checklist-item .label-text {
|
||||
color: var(--text-color);
|
||||
flex-grow: 1;
|
||||
font-weight: var(--font-weight-demi-bold);
|
||||
padding-left: var(--padding-sm);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.checklist-item:hover {
|
||||
background-color: var(--main-fg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.checklist-item input[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.checklist-item input[type="checkbox"]:checked ~ .custom-checkbox {
|
||||
background-color: var(--success-bg);
|
||||
}
|
||||
|
||||
.checklist-item input[type="checkbox"]:checked ~ .custom-checkbox::before {
|
||||
transform: translateX(24px);
|
||||
}
|
||||
|
||||
.color-label input[type="color"] {
|
||||
appearance: none;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
height: 40px;
|
||||
overflow: hidden;
|
||||
transition: box-shadow var(--transition-fast);
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.color-label input[type="color"]::-moz-color-swatch {
|
||||
border: var(--border-width-base) solid var(--text-color);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.color-label input[type="color"]::-webkit-color-swatch {
|
||||
border: var(--border-width-base) solid var(--text-color);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.color-label:hover input[type="color"] {
|
||||
box-shadow: 0 0 8px var(--thumb-color);
|
||||
}
|
||||
|
||||
.color-section,
|
||||
.upload-section {
|
||||
display: grid;
|
||||
gap: var(--gap-md);
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.delete-theme-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--danger-fg);
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
padding: 0 0.5rem;
|
||||
transition: color var(--transition-fast), transform 0.2s;
|
||||
}
|
||||
|
||||
.delete-theme-button:hover {
|
||||
color: var(--danger-hover-bg);
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.discord-username-input {
|
||||
background-color: var(--input-bg);
|
||||
border: var(--border-style-input);
|
||||
border-radius: var(--border-radius-base);
|
||||
box-sizing: border-box;
|
||||
color: var(--text-color);
|
||||
margin-top: var(--border-radius-lg);
|
||||
padding: var(--padding-sm);
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.download-theme-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-confirm);
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
padding: 0 0.5rem;
|
||||
transition: color var(--transition-fast), transform 0.2s;
|
||||
}
|
||||
|
||||
.download-theme-button:hover {
|
||||
color: var(--color-confirm-hover);
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.file-clear-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--danger-fg);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
padding: 0 0.4rem;
|
||||
flex-shrink: 0;
|
||||
transition: color var(--transition-fast), transform 0.2s;
|
||||
}
|
||||
.file-clear-button:hover {
|
||||
color: var(--danger-hover-bg);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.file-name-display {
|
||||
color: var(--text-muted);
|
||||
flex-grow: 1;
|
||||
font-size: 0.9em;
|
||||
font-style: italic;
|
||||
margin-right: var(--margin-sm);
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-align: right;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
label.file-upload-button {
|
||||
background-color: var(--success-bg);
|
||||
border-radius: var(--border-radius-sm);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
font-size: 0.9em;
|
||||
padding: 0.3rem 0.6rem;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-upload-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.file-upload-label {
|
||||
align-items: center;
|
||||
background-color: var(--secondary-bg);
|
||||
border-radius: var(--border-radius-md);
|
||||
display: flex;
|
||||
min-height: 2.5rem;
|
||||
overflow: hidden;
|
||||
padding: var(--padding-sm);
|
||||
transition: background-color 0.2s, box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.file-upload-label:hover {
|
||||
background-color: var(--main-fg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.file-upload-label:hover .file-upload-button {
|
||||
background-color: var(--sidebar-bg);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.file-upload-text {
|
||||
color: var(--text-color);
|
||||
flex-shrink: 0;
|
||||
font-weight: var(--font-weight-demi-bold);
|
||||
margin-right: var(--margin-sm);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.help-icon {
|
||||
background-color: var(--main-fg);
|
||||
border-radius: 50%;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
height: 1.2em;
|
||||
line-height: 1.2em;
|
||||
margin-left: 0.5em;
|
||||
text-align: center;
|
||||
transition: background-color var(--transition-fast), transform var(--transition-fast);
|
||||
width: 1.2em;
|
||||
}
|
||||
|
||||
.help-icon:hover {
|
||||
background-color: var(--success-hover-bg);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.manage-themes-button {
|
||||
background-color: var(--color-confirm);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-lg);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-bold);
|
||||
overflow: hidden;
|
||||
padding: 0.75rem 1.5rem;
|
||||
transition: background-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.manage-themes-button:hover {
|
||||
background-color: var(--color-confirm-hover);
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.manage-themes-modal {
|
||||
animation: modalFadeIn 0.3s ease;
|
||||
max-width: var(--width-xl);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.manage-themes-tabs {
|
||||
border: 1px solid var(--sidebar-border-color);
|
||||
border-radius: var(--border-radius-md);
|
||||
display: flex;
|
||||
margin-bottom: 1rem;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
background-color: var(--success-bg);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-lg);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-bold);
|
||||
overflow: hidden;
|
||||
padding: 0.75rem 1.5rem;
|
||||
transition: background-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.save-button:hover {
|
||||
background-color: var(--success-hover-bg);
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.save-button-wrapper {
|
||||
display: flex;
|
||||
gap: var(--gap-md);
|
||||
justify-content: center;
|
||||
margin-top: var(--margin-lg);
|
||||
}
|
||||
|
||||
.signal-type-toggle {
|
||||
display: flex;
|
||||
gap: var(--gap-sm);
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
background-color: var(--accent-bg);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-lg);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-bold);
|
||||
overflow: hidden;
|
||||
padding: 0.75rem 1.5rem;
|
||||
transition: background-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.submit-button:hover {
|
||||
background-color: var(--accent-hover-bg);
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
background-color: var(--input-bg);
|
||||
border: none;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
font-weight: var(--font-weight-demi-bold);
|
||||
padding: 0.75rem;
|
||||
transition: background-color var(--transition-fast), box-shadow var(--transition-fast), color var(--transition-fast), transform var(--transition-fast);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
background-color: var(--main-fg);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tab-button:not(:last-child) {
|
||||
border-right: 1px solid var(--sidebar-border-color);
|
||||
}
|
||||
|
||||
.tab-button:not(.active):hover {
|
||||
background-color: var(--main-fg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.theme-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
flex-grow: 1;
|
||||
font-weight: var(--font-weight-demi-bold);
|
||||
text-align: left;
|
||||
transition: color var(--transition-fast);
|
||||
}
|
||||
|
||||
.theme-button:hover {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.theme-item {
|
||||
align-items: center;
|
||||
background-color: var(--input-bg);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem;
|
||||
transition: background-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.theme-item:not(:last-child) {
|
||||
border-bottom: 1px solid var(--sidebar-border-color);
|
||||
}
|
||||
|
||||
.theme-item:hover {
|
||||
background-color: var(--main-fg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.theme-maker-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.theme-maker-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-md);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.theme-maker-form .color-label {
|
||||
align-items: center;
|
||||
background-color: var(--secondary-bg);
|
||||
border-radius: var(--border-radius-md);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-weight: var(--font-weight-demi-bold);
|
||||
justify-content: space-between;
|
||||
padding: var(--padding-sm);
|
||||
transition: box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.theme-maker-form .color-label:hover {
|
||||
box-shadow: var(--shadow-sm);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.theme-maker-main-title {
|
||||
background-color: var(--input-bg);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
box-sizing: border-box;
|
||||
color: var(--text-color);
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin-bottom: var(--margin-base);
|
||||
padding: var(--padding-sm);
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.theme-maker-main-widget {
|
||||
background-color: var(--secondary-bg);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-md);
|
||||
color: var(--main-fg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: var(--padding-xl);
|
||||
max-width: var(--width-xxxxl);
|
||||
padding: var(--padding-lg);
|
||||
transition: box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.theme-maker-main-widget:hover {
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.theme-maker-sub-widgets {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--gap-lg);
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.theme-maker-title {
|
||||
background-color: var(--main-fg);
|
||||
border-radius: var(--border-radius-md);
|
||||
color: var(--text-color);
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin: 0 auto var(--margin-base);
|
||||
padding: var(--padding-sm) var(--padding-lg);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.theme-maker-widget {
|
||||
background-color: var(--input-bg);
|
||||
border-radius: var(--border-radius-md);
|
||||
flex: 1 1 var(--width-md);
|
||||
overflow: visible;
|
||||
padding: var(--padding-base);
|
||||
transition: box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.theme-maker-widget:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.theme-name-label {
|
||||
color: var(--text-color);
|
||||
display: block;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin-bottom: var(--margin-xs);
|
||||
}
|
||||
|
||||
.theme-name-section {
|
||||
box-sizing: border-box;
|
||||
margin-bottom: var(--margin-lg);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.themes-list {
|
||||
border: 1px solid var(--sidebar-border-color);
|
||||
border-radius: var(--border-radius-md);
|
||||
max-height: 300px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
background-color: var(--secondary-bg);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-md);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-normal);
|
||||
padding: var(--padding-sm);
|
||||
transition: background-color var(--transition-fast), box-shadow var(--transition-fast), color var(--transition-fast), transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.toggle-button.active {
|
||||
background-color: var(--main-fg);
|
||||
color: var(--text-color);
|
||||
font-weight: var(--font-weight-demi-bold);
|
||||
}
|
||||
|
||||
.toggle-button:not(.active):hover {
|
||||
background-color: var(--main-fg);
|
||||
box-shadow: var(--shadow-md);
|
||||
font-weight: var(--font-weight-demi-bold);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.turn-signal-help-text {
|
||||
background-color: var(--secondary-bg);
|
||||
border-radius: var(--border-radius-md);
|
||||
margin-top: 0.5em;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.turn-signal-help-text p {
|
||||
font-weight: normal;
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
.turn-signal-input {
|
||||
-moz-appearance: textfield;
|
||||
background-color: var(--secondary-bg);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-md);
|
||||
box-sizing: border-box;
|
||||
color: var(--text-color);
|
||||
font-size: var(--font-size-base);
|
||||
padding: var(--padding-sm);
|
||||
transition: box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.turn-signal-input::-webkit-inner-spin-button,
|
||||
.turn-signal-input::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.turn-signal-label {
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-demi-bold);
|
||||
}
|
||||
|
||||
.turn-signal-length-section,
|
||||
.turn-signal-style-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-xs);
|
||||
}
|
||||
|
||||
#themeName,
|
||||
#submitThemeName {
|
||||
background-color: var(--input-bg);
|
||||
border: var(--border-style-input);
|
||||
border-radius: var(--border-radius-sm);
|
||||
box-sizing: border-box;
|
||||
color: var(--text-color);
|
||||
font-size: var(--font-size-sm);
|
||||
padding: var(--padding-sm);
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#themeName:focus,
|
||||
#themeName:hover,
|
||||
#submitThemeName:focus,
|
||||
#submitThemeName:hover,
|
||||
.discord-username-input:focus,
|
||||
.discord-username-input:hover,
|
||||
.turn-signal-input:focus,
|
||||
.turn-signal-input:hover {
|
||||
border-color: var(--thumb-color);
|
||||
box-shadow: 0 0 0 2px var(--thumb-color), 0 0 8px var(--thumb-color);
|
||||
outline: none;
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.sequence-order-button {
|
||||
background-color: var(--main-fg);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-md);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-demi-bold);
|
||||
margin-top: var(--margin-sm);
|
||||
padding: var(--padding-sm) var(--padding-base);
|
||||
text-align: center;
|
||||
transition: background-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sequence-order-button:hover {
|
||||
background-color: var(--main-fg);
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.draggable-list {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.draggable-item {
|
||||
align-items: center;
|
||||
background-color: var(--input-bg);
|
||||
border: 1px solid var(--sidebar-border-color);
|
||||
border-radius: var(--border-radius-sm);
|
||||
cursor: move;
|
||||
display: flex;
|
||||
margin-bottom: 5px;
|
||||
padding: 10px;
|
||||
transition: background-color 0.2s, opacity 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.draggable-item.dragging {
|
||||
opacity: 0.5;
|
||||
background-color: var(--main-fg);
|
||||
}
|
||||
|
||||
.draggable-item:hover {
|
||||
background-color: var(--main-fg);
|
||||
}
|
||||
|
||||
.draggable-item.drop-before::before,
|
||||
.draggable-item.drop-after::after {
|
||||
bottom: -2px;
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background-color: var(--success-bg);
|
||||
}
|
||||
|
||||
.draggable-item.drop-before::before {
|
||||
top: -2px;
|
||||
}
|
||||
|
||||
.sequential-image-preview {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
object-fit: contain;
|
||||
margin-right: 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@keyframes modalFadeIn {
|
||||
from { opacity: 0; transform: scale(0.95); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) and (orientation: portrait) {
|
||||
.manage-themes-tabs {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.manage-themes-tabs .tab-button {
|
||||
border-bottom: 1px solid var(--sidebar-border-color);
|
||||
border-right: 1px solid var(--sidebar-border-color);
|
||||
flex: 1 0 33.333%;
|
||||
}
|
||||
|
||||
.manage-themes-tabs .tab-button:nth-child(3n) {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.save-button-wrapper {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.save-button-wrapper > button {
|
||||
flex-basis: calc(50% - 1rem);
|
||||
margin-bottom: var(--gap-sm);
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@
|
||||
<link rel="stylesheet" href="/assets/components/tools/doors.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/error_logs.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/speed_limits.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/theme_maker.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/tmux.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/toggles.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/tsk_manager.css">
|
||||
|
||||
@@ -6,6 +6,7 @@ from io import BytesIO
|
||||
from pathlib import Path
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
import base64
|
||||
import errno
|
||||
import hashlib
|
||||
import json
|
||||
@@ -13,6 +14,7 @@ import os
|
||||
import re
|
||||
import requests
|
||||
import secrets
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import time
|
||||
@@ -28,11 +30,18 @@ from openpilot.system.loggerd.deleter import PRESERVE_ATTR_NAME, PRESERVE_ATTR_V
|
||||
from openpilot.system.version import get_build_metadata
|
||||
from panda import Panda
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import delete_file, get_lock_status, run_cmd
|
||||
from openpilot.frogpilot.common.frogpilot_variables import ERROR_LOGS_PATH, EXCLUDED_KEYS, SCREEN_RECORDINGS_PATH,\
|
||||
frogpilot_default_params, params, update_frogpilot_toggles
|
||||
from openpilot.frogpilot.assets.theme_manager import HOLIDAY_THEME_PATH, THEME_COMPONENT_PARAMS
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import delete_file, get_lock_status, run_cmd, extract_tar
|
||||
from openpilot.frogpilot.common.frogpilot_variables import ACTIVE_THEME_PATH, ERROR_LOGS_PATH, EXCLUDED_KEYS, RESOURCES_REPO, SCREEN_RECORDINGS_PATH, THEME_SAVE_PATH,\
|
||||
frogpilot_default_params, params, params_memory, update_frogpilot_toggles
|
||||
from openpilot.frogpilot.system.the_pond import utilities
|
||||
|
||||
DISCORD_WEBHOOK_URL = os.getenv("DISCORD_WEBHOOK_URL")
|
||||
|
||||
GITLAB_API = "https://gitlab.com/api/v4"
|
||||
GITLAB_SUBMISSIONS_PROJECT_ID = "71992109"
|
||||
GITLAB_TOKEN = os.environ.get("GITLAB_TOKEN", "")
|
||||
|
||||
FOOTAGE_PATHS = [
|
||||
Paths.log_root(HD=True, raw=True),
|
||||
Paths.log_root(konik=True, raw=True),
|
||||
@@ -71,7 +80,8 @@ def setup(app):
|
||||
|
||||
while True:
|
||||
with Panda(disable_checks=True) as panda:
|
||||
panda.set_safety_mode(panda.SAFETY_TOYOTA)
|
||||
if not params.get_bool("IsOnroad"):
|
||||
panda.set_safety_mode(panda.SAFETY_TOYOTA)
|
||||
panda.can_send(0x750, LOCK_CMD, 0)
|
||||
|
||||
time.sleep(1)
|
||||
@@ -89,7 +99,8 @@ def setup(app):
|
||||
|
||||
while True:
|
||||
with Panda(disable_checks=True) as panda:
|
||||
panda.set_safety_mode(panda.SAFETY_TOYOTA)
|
||||
if not params.get_bool("IsOnroad"):
|
||||
panda.set_safety_mode(panda.SAFETY_TOYOTA)
|
||||
panda.can_send(0x750, UNLOCK_CMD, 0)
|
||||
|
||||
time.sleep(1)
|
||||
@@ -293,6 +304,10 @@ def setup(app):
|
||||
def get_param():
|
||||
return params.get(request.args.get("key")) or "", 200
|
||||
|
||||
@app.route("/api/params_memory", methods=["GET"])
|
||||
def get_param_memory():
|
||||
return params_memory.get(request.args.get("key")) or "", 200
|
||||
|
||||
@app.route("/api/routes", methods=["GET"])
|
||||
def list_routes():
|
||||
def generate():
|
||||
@@ -306,6 +321,27 @@ def setup(app):
|
||||
try:
|
||||
result = future.result()
|
||||
yield f"data: {json.dumps({'routes': [result]})}\n\n"
|
||||
|
||||
path, name = futures[future]
|
||||
segments = utilities.get_segments_in_route(name, path)
|
||||
if segments:
|
||||
for camera, cam_file in {
|
||||
"forward": "fcamera.hevc",
|
||||
"wide": "ecamera.hevc",
|
||||
"driver": "dcamera.hevc"
|
||||
}.items():
|
||||
input_files = [
|
||||
os.path.join(path, seg, cam_file)
|
||||
for seg in segments
|
||||
if os.path.exists(os.path.join(path, seg, cam_file))
|
||||
]
|
||||
if input_files:
|
||||
executor.submit(
|
||||
utilities.ffmpeg_concat_segments_to_mp4,
|
||||
input_files,
|
||||
f"{name}-{camera}"
|
||||
)
|
||||
|
||||
except Exception as exception:
|
||||
print(f"Error processing route: {exception}")
|
||||
yield f"data: {json.dumps({'progress': processed, 'total': total})}\n\n"
|
||||
@@ -370,6 +406,32 @@ def setup(app):
|
||||
return {"message": "Route unpreserved!"}, 200
|
||||
return {"error": "Route not found"}, 404
|
||||
|
||||
@app.route("/video/<name>/combined", methods=["GET"])
|
||||
def get_combined_route_video(name):
|
||||
camera = request.args.get("camera", "forward")
|
||||
for footage_path in FOOTAGE_PATHS:
|
||||
segments = utilities.get_segments_in_route(name, footage_path)
|
||||
if segments:
|
||||
cam_file = {
|
||||
"forward": "fcamera.hevc",
|
||||
"wide": "ecamera.hevc",
|
||||
"driver": "dcamera.hevc",
|
||||
}.get(camera, "fcamera.hevc")
|
||||
|
||||
input_files = [
|
||||
os.path.join(footage_path, seg, cam_file)
|
||||
for seg in segments
|
||||
if os.path.exists(os.path.join(footage_path, seg, cam_file))
|
||||
]
|
||||
|
||||
if not input_files:
|
||||
return {"error": "No video files found"}, 404
|
||||
|
||||
mp4_file = utilities.ffmpeg_concat_segments_to_mp4(input_files, cache_key=f"{name}-{camera}")
|
||||
return send_file(mp4_file, mimetype="video/mp4")
|
||||
|
||||
return {"error": "Route not found"}, 404
|
||||
|
||||
@app.route("/api/routes/<name>", methods=["GET"])
|
||||
def get_route(name):
|
||||
for footage_path in FOOTAGE_PATHS:
|
||||
@@ -652,7 +714,9 @@ def setup(app):
|
||||
os.makedirs(state, exist_ok=True)
|
||||
|
||||
run_cmd(["curl", "-fsSL", tgz_url, "-o", tgz_path], "Downloaded Tailscale archive.", "Failed to download Tailscale archive.")
|
||||
run_cmd(["tar", "xzf", tgz_path, "-C", base], "Extracted Tailscale archive.", "Failed to extract Tailscale archive.")
|
||||
|
||||
extract_tar(tgz_path, base)
|
||||
|
||||
run_cmd(["cp", f"{bin_dir}/tailscale", f"{base}/tailscale"], "Copied tailscale binary.", "Failed to copy tailscale binary.")
|
||||
run_cmd(["cp", f"{bin_dir}/tailscaled", f"{base}/tailscaled"], "Copied tailscaled binary.", "Failed to copy tailscaled binary.")
|
||||
run_cmd(["chmod", "+x", f"{base}/tailscale", f"{base}/tailscaled"], "Made binaries executable.", "Failed to chmod binaries.")
|
||||
@@ -740,6 +804,591 @@ def setup(app):
|
||||
|
||||
return jsonify({"message": "Tailscale uninstalled!"}), 200
|
||||
|
||||
@app.route("/api/themes", methods=["POST"])
|
||||
def save_theme_route():
|
||||
theme_path, error = utilities.create_theme(request.form, request.files)
|
||||
if error:
|
||||
return jsonify({"message": error}), 400
|
||||
return jsonify({"message": f'Theme "{request.form.get("themeName")}" saved!'}), 200
|
||||
|
||||
@app.route("/api/themes/download_asset", methods=["POST"])
|
||||
def start_download_asset():
|
||||
data = request.get_json() or {}
|
||||
raw_component = (data.get("component") or "").strip()
|
||||
display_name = (data.get("name") or "").strip()
|
||||
if not raw_component or not display_name:
|
||||
return jsonify({"error": "Missing component or name"}), 400
|
||||
|
||||
component = "steering_wheels" if raw_component == "steering_wheel" else ("signals" if raw_component == "turn_signals" else raw_component)
|
||||
mem_key = THEME_COMPONENT_PARAMS.get(component)
|
||||
if not mem_key:
|
||||
return jsonify({"error": "Unknown component"}), 400
|
||||
|
||||
slug = display_name.lower().replace("(", "").replace(")", "").replace(" ", "_")
|
||||
|
||||
params_memory.put(mem_key, slug)
|
||||
params_memory.put("ThemeDownloadProgress", "Downloading...")
|
||||
|
||||
return jsonify({"message": "Download started", "component": component, "param": mem_key, "slug": slug}), 200
|
||||
|
||||
@app.route("/api/themes/apply", methods=["POST"])
|
||||
def apply_theme():
|
||||
form_data = request.form.to_dict(flat=True)
|
||||
files = request.files
|
||||
|
||||
if not form_data.get("themeName"):
|
||||
form_data["themeName"] = f"tmp_{secrets.token_hex(8)}"
|
||||
|
||||
temp_path, error = utilities.create_theme(form_data, files, temporary=True)
|
||||
if error:
|
||||
return {"error": error}, 400
|
||||
|
||||
save_checklist = json.loads(form_data.get("saveChecklist", "{}"))
|
||||
|
||||
if save_checklist.get("colors"):
|
||||
asset_location = temp_path / "colors"
|
||||
save_location = ACTIVE_THEME_PATH / "colors"
|
||||
if save_location.exists() or save_location.is_symlink():
|
||||
delete_file(save_location)
|
||||
if asset_location.exists():
|
||||
save_location.parent.mkdir(parents=True, exist_ok=True)
|
||||
save_location.symlink_to(asset_location, target_is_directory=True)
|
||||
|
||||
if save_checklist.get("distance_icons"):
|
||||
asset_location = temp_path / "distance_icons"
|
||||
save_location = ACTIVE_THEME_PATH / "distance_icons"
|
||||
if save_location.exists() or save_location.is_symlink():
|
||||
delete_file(save_location)
|
||||
if asset_location.exists():
|
||||
save_location.parent.mkdir(parents=True, exist_ok=True)
|
||||
save_location.symlink_to(asset_location, target_is_directory=True)
|
||||
|
||||
if save_checklist.get("icons"):
|
||||
asset_location = temp_path / "icons"
|
||||
save_location = ACTIVE_THEME_PATH / "icons"
|
||||
if save_location.exists() or save_location.is_symlink():
|
||||
delete_file(save_location)
|
||||
if asset_location.exists():
|
||||
save_location.parent.mkdir(parents=True, exist_ok=True)
|
||||
save_location.symlink_to(asset_location, target_is_directory=True)
|
||||
|
||||
if save_checklist.get("sounds"):
|
||||
asset_location = temp_path / "sounds"
|
||||
save_location = ACTIVE_THEME_PATH / "sounds"
|
||||
if save_location.exists() or save_location.is_symlink():
|
||||
delete_file(save_location)
|
||||
if asset_location.exists():
|
||||
save_location.parent.mkdir(parents=True, exist_ok=True)
|
||||
save_location.symlink_to(asset_location, target_is_directory=True)
|
||||
|
||||
if save_checklist.get("turn_signals"):
|
||||
asset_location = temp_path / "signals"
|
||||
save_location = ACTIVE_THEME_PATH / "signals"
|
||||
if save_location.exists() or save_location.is_symlink():
|
||||
delete_file(save_location)
|
||||
if asset_location.exists():
|
||||
save_location.parent.mkdir(parents=True, exist_ok=True)
|
||||
save_location.symlink_to(asset_location, target_is_directory=True)
|
||||
|
||||
wheel_location = temp_path / "WheelIcon"
|
||||
wheel_save_location = ACTIVE_THEME_PATH / "steering_wheel"
|
||||
if wheel_location.exists():
|
||||
if wheel_save_location.exists():
|
||||
delete_file(wheel_save_location)
|
||||
|
||||
wheel_save_location.mkdir(parents=True, exist_ok=True)
|
||||
for file in wheel_location.iterdir():
|
||||
destination_file = wheel_save_location / file.name
|
||||
delete_file(destination_file)
|
||||
destination_file.symlink_to(file)
|
||||
|
||||
params.put_bool("PersonalizeOpenpilot", True)
|
||||
params_memory.put_bool("UseActiveTheme", True)
|
||||
|
||||
update_frogpilot_toggles()
|
||||
return {"message": "Theme applied successfully!"}, 200
|
||||
|
||||
@app.route("/api/themes/asset/<path:theme>/<path:asset_path>")
|
||||
def get_theme_asset(theme, asset_path):
|
||||
theme_type = request.args.get("type", "")
|
||||
|
||||
if theme_type == "active" or theme == "__active__":
|
||||
file_path = ACTIVE_THEME_PATH / asset_path
|
||||
elif asset_path.startswith("steering_wheels/"):
|
||||
file_path = THEME_SAVE_PATH / asset_path
|
||||
elif asset_path.startswith("steering_wheel/") and "holiday" in theme_type:
|
||||
file_path = HOLIDAY_THEME_PATH / theme / asset_path
|
||||
else:
|
||||
base_dir = HOLIDAY_THEME_PATH / theme if "holiday" in theme_type else THEME_SAVE_PATH / "theme_packs" / theme
|
||||
file_path = base_dir / asset_path
|
||||
|
||||
if not file_path.exists():
|
||||
return "File not found", 404
|
||||
|
||||
return send_file(file_path, as_attachment=False)
|
||||
|
||||
@app.route("/api/themes/delete/<path:theme_path_str>", methods=["DELETE"])
|
||||
def delete_theme(theme_path_str):
|
||||
theme_type = request.args.get("type", "user")
|
||||
component = (request.args.get("component") or "").strip()
|
||||
|
||||
if theme_type == "holiday":
|
||||
return jsonify({"message": "Cannot delete holiday themes."}), 403
|
||||
|
||||
if theme_type == "steering_wheel":
|
||||
wheel_path = THEME_SAVE_PATH / "steering_wheels" / theme_path_str
|
||||
if wheel_path.exists():
|
||||
delete_file(wheel_path)
|
||||
return jsonify({"message": f'Steering wheel "{utilities.normalize_theme_name(wheel_path.stem)}" deleted!'}), 200
|
||||
return jsonify({"message": "Steering wheel not found..."}), 404
|
||||
|
||||
theme_path = THEME_SAVE_PATH / "theme_packs" / theme_path_str
|
||||
if not theme_path.is_dir():
|
||||
return jsonify({"message": "Theme not found..."}), 404
|
||||
|
||||
if component:
|
||||
allowed = {"colors", "distance_icons", "icons", "sounds", "signals"}
|
||||
if component not in allowed:
|
||||
return jsonify({"message": "Unknown component..."}), 400
|
||||
|
||||
target = theme_path / component
|
||||
if not target.exists():
|
||||
return jsonify({"message": f'Component "{component}" not found in theme...'}), 404
|
||||
|
||||
delete_file(target)
|
||||
|
||||
return jsonify({"message": f'Removed {component.replace("_", " ")} from "{utilities.normalize_theme_name(theme_path.name)}"!'}), 200
|
||||
|
||||
delete_file(theme_path)
|
||||
return jsonify({"message": f'Theme "{utilities.normalize_theme_name(theme_path.name)}" deleted!'}), 200
|
||||
|
||||
@app.route("/api/themes/default", methods=["GET"])
|
||||
def get_default_theme():
|
||||
theme_data = {
|
||||
"colors": {},
|
||||
"images": {},
|
||||
"sounds": {},
|
||||
"turnSignalLength": 100,
|
||||
"turnSignalType": "Single Image",
|
||||
"sequentialImages": [],
|
||||
"theme_names": {}
|
||||
}
|
||||
|
||||
if not params.get_bool("PersonalizeOpenpilot"):
|
||||
theme_data["theme_names"] = {
|
||||
"colors": "Stock",
|
||||
"distanceIcons": "Stock",
|
||||
"icons": "Stock",
|
||||
"sounds": "Stock",
|
||||
"turnSignals": "Stock",
|
||||
"steeringWheel": "Stock"
|
||||
}
|
||||
else:
|
||||
theme_param_map = {
|
||||
"CustomColors": "colors",
|
||||
"CustomDistanceIcons": "distanceIcons",
|
||||
"CustomIcons": "icons",
|
||||
"CustomSounds": "sounds",
|
||||
"CustomSignals": "turnSignals",
|
||||
"WheelIcon": "steeringWheel"
|
||||
}
|
||||
for param, theme_key in theme_param_map.items():
|
||||
param_value = params.get(param, encoding="utf-8")
|
||||
if param_value:
|
||||
theme_data["theme_names"][theme_key] = utilities.normalize_theme_name(param_value)
|
||||
|
||||
colors_path = ACTIVE_THEME_PATH / "colors" / "colors.json"
|
||||
if colors_path.exists():
|
||||
with open(colors_path, "r") as f:
|
||||
theme_data["colors"] = json.load(f)
|
||||
|
||||
signals_dir = ACTIVE_THEME_PATH / "signals"
|
||||
if signals_dir.exists():
|
||||
sequential_files = sorted([f.name for f in signals_dir.glob("turn_signal_*.png") if "blindspot" not in f.name.lower()])
|
||||
if sequential_files:
|
||||
theme_data["sequentialImages"] = sequential_files
|
||||
theme_data["turnSignalType"] = "Sequential"
|
||||
|
||||
theme_data["turnSignalStyle"] = "Traditional"
|
||||
theme_data["turnSignalLength"] = 100
|
||||
|
||||
for file in os.listdir(signals_dir):
|
||||
if not any(file.endswith(ext) for ext in [".png", ".gif", ".jpg", ".jpeg"]):
|
||||
parts = file.split("_")
|
||||
if len(parts) == 2:
|
||||
theme_data["turnSignalStyle"] = parts[0].capitalize()
|
||||
try:
|
||||
theme_data["turnSignalLength"] = int(parts[1])
|
||||
except ValueError:
|
||||
pass
|
||||
break
|
||||
|
||||
exts = [".png", ".gif", ".jpg", ".jpeg"]
|
||||
for ext in exts:
|
||||
p = signals_dir / f"turn_signal{ext}"
|
||||
if p.exists():
|
||||
theme_data["images"]["turnSignal"] = f"turn_signal{ext}"
|
||||
break
|
||||
for ext in exts:
|
||||
p = signals_dir / f"turn_signal_blindspot{ext}"
|
||||
if p.exists():
|
||||
theme_data["images"]["turnSignalBlindspot"] = f"turn_signal_blindspot{ext}"
|
||||
break
|
||||
|
||||
icons_path = ACTIVE_THEME_PATH / "icons"
|
||||
if icons_path.exists() and icons_path.is_dir():
|
||||
for file in os.listdir(icons_path):
|
||||
if Path(file).stem == "button_settings":
|
||||
theme_data["images"]["settingsButton"] = file
|
||||
elif Path(file).stem == "button_home":
|
||||
theme_data["images"]["homeButton"] = file
|
||||
|
||||
wheel_path = ACTIVE_THEME_PATH / "steering_wheel"
|
||||
if wheel_path.exists() and wheel_path.is_dir():
|
||||
wheel_files = list(wheel_path.glob("wheel.*"))
|
||||
if wheel_files:
|
||||
theme_data["images"]["steeringWheel"] = wheel_files[0].name
|
||||
|
||||
distance_icons_path = ACTIVE_THEME_PATH / "distance_icons"
|
||||
if distance_icons_path.exists() and distance_icons_path.is_dir():
|
||||
theme_data["images"]["distanceIcons"] = {}
|
||||
for file in os.listdir(distance_icons_path):
|
||||
key = Path(file).stem
|
||||
if key in ["traffic", "aggressive", "standard", "relaxed"]:
|
||||
theme_data["images"]["distanceIcons"][key] = file
|
||||
|
||||
sounds_path = ACTIVE_THEME_PATH / "sounds"
|
||||
if sounds_path.exists() and sounds_path.is_dir():
|
||||
valid_sound_keys = ["engage", "disengage", "prompt", "startup"]
|
||||
for file in os.listdir(sounds_path):
|
||||
stem = Path(file).stem
|
||||
if stem in valid_sound_keys:
|
||||
theme_data["sounds"][stem] = file
|
||||
|
||||
return jsonify(theme_data)
|
||||
|
||||
@app.route("/api/themes/download", methods=["POST"])
|
||||
def download_theme_route():
|
||||
theme_path, error = utilities.create_theme(request.form, request.files, temporary=True)
|
||||
if error:
|
||||
return jsonify({"message": error}), 400
|
||||
|
||||
sane_theme_name = utilities.normalize_theme_name(request.form.get("themeName"), for_path=True)
|
||||
|
||||
archive_path = shutil.make_archive(str(theme_path.parent / sane_theme_name), "zip", theme_path.parent, sane_theme_name)
|
||||
|
||||
memory_file = BytesIO()
|
||||
with open(archive_path, "rb") as f:
|
||||
memory_file.write(f.read())
|
||||
memory_file.seek(0)
|
||||
|
||||
delete_file(theme_path.parent)
|
||||
|
||||
return send_file(memory_file, download_name=f'{sane_theme_name}.zip', as_attachment=True)
|
||||
|
||||
@app.route("/api/themes/list", methods=["GET"])
|
||||
def list_themes():
|
||||
all_themes = []
|
||||
themes_path = THEME_SAVE_PATH / "theme_packs"
|
||||
|
||||
if themes_path.exists():
|
||||
for theme_dir in themes_path.iterdir():
|
||||
if theme_dir.is_dir():
|
||||
is_user_created = "-user_created" in theme_dir.name
|
||||
components = utilities.check_theme_components(theme_dir)
|
||||
all_themes.append({
|
||||
"name": utilities.normalize_theme_name(theme_dir.name),
|
||||
"path": theme_dir.name,
|
||||
"type": "user" if is_user_created else "standard",
|
||||
"is_user_created": is_user_created,
|
||||
**components
|
||||
})
|
||||
|
||||
if HOLIDAY_THEME_PATH.exists():
|
||||
for theme_dir in HOLIDAY_THEME_PATH.iterdir():
|
||||
if theme_dir.is_dir():
|
||||
components = utilities.check_theme_components(theme_dir)
|
||||
all_themes.append({
|
||||
"name": utilities.normalize_theme_name(theme_dir.name),
|
||||
"path": theme_dir.name,
|
||||
"type": "holiday",
|
||||
"is_user_created": False,
|
||||
**components
|
||||
})
|
||||
|
||||
wheels_path = THEME_SAVE_PATH / "steering_wheels"
|
||||
if wheels_path.exists():
|
||||
for wheel_file in wheels_path.iterdir():
|
||||
all_themes.append({
|
||||
"name": utilities.normalize_theme_name(wheel_file.stem),
|
||||
"path": wheel_file.name,
|
||||
"type": "steering_wheel",
|
||||
"is_user_created": "-user_created" in wheel_file.name,
|
||||
"hasSteeringWheel": True,
|
||||
})
|
||||
|
||||
return jsonify({"themes": sorted(all_themes, key=lambda x: x['name'])})
|
||||
|
||||
@app.route("/api/themes/load/<path:theme_path>")
|
||||
def load_theme(theme_path):
|
||||
theme_type = request.args.get("type", "")
|
||||
theme_dir = HOLIDAY_THEME_PATH / theme_path if "holiday" in theme_type else THEME_SAVE_PATH / "theme_packs" / theme_path
|
||||
|
||||
response_data = {
|
||||
"colors": None,
|
||||
"images": {},
|
||||
"sounds": {},
|
||||
"sequentialImages": [],
|
||||
"turnSignalType": "Single Image",
|
||||
"turnSignalStyle": "Static",
|
||||
"turnSignalLength": 100
|
||||
}
|
||||
|
||||
colors_file = theme_dir / "colors" / "colors.json"
|
||||
if colors_file.exists():
|
||||
with open(colors_file) as f:
|
||||
response_data["colors"] = json.load(f)
|
||||
|
||||
icons_dir = theme_dir / "icons"
|
||||
if icons_dir.exists():
|
||||
if (icons_dir / "button_home.gif").exists():
|
||||
response_data["images"]["homeButton"] = {
|
||||
"filename": "button_home.gif",
|
||||
"path": "icons/button_home.gif"
|
||||
}
|
||||
if (icons_dir / "button_settings.png").exists():
|
||||
response_data["images"]["settingsButton"] = {
|
||||
"filename": "button_settings.png",
|
||||
"path": "icons/button_settings.png"
|
||||
}
|
||||
|
||||
distance_dir = theme_dir / "distance_icons"
|
||||
if distance_dir.exists():
|
||||
response_data["images"]["distanceIcons"] = {}
|
||||
exts = [".png", ".gif", ".jpg", ".jpeg"]
|
||||
for name in ["aggressive", "relaxed", "standard", "traffic"]:
|
||||
for ext in exts:
|
||||
p = distance_dir / f"{name}{ext}"
|
||||
if p.exists():
|
||||
response_data["images"]["distanceIcons"][name] = {
|
||||
"filename": f"{name}{ext}",
|
||||
"path": f"distance_icons/{name}{ext}"
|
||||
}
|
||||
break
|
||||
|
||||
signals_dir = theme_dir / "signals"
|
||||
if signals_dir.exists():
|
||||
sequential_files = sorted([f.name for f in signals_dir.glob("turn_signal_*.png") if "blindspot" not in f.name.lower()])
|
||||
if sequential_files:
|
||||
response_data["sequentialImages"] = sequential_files
|
||||
response_data["turnSignalType"] = "Sequential"
|
||||
|
||||
response_data["turnSignalStyle"] = "Traditional"
|
||||
response_data["turnSignalLength"] = 100
|
||||
|
||||
for file in os.listdir(signals_dir):
|
||||
if not any(file.endswith(ext) for ext in [".png", ".gif", ".jpg", ".jpeg"]):
|
||||
parts = file.split("_")
|
||||
if len(parts) == 2:
|
||||
response_data["turnSignalStyle"] = parts[0].capitalize()
|
||||
try:
|
||||
response_data["turnSignalLength"] = int(parts[1])
|
||||
except ValueError:
|
||||
pass
|
||||
break
|
||||
|
||||
exts = [".png", ".gif", ".jpg", ".jpeg"]
|
||||
for ext in exts:
|
||||
p = signals_dir / f"turn_signal{ext}"
|
||||
if p.exists():
|
||||
response_data["images"]["turnSignal"] = {
|
||||
"filename": f"turn_signal{ext}",
|
||||
"path": f"signals/turn_signal{ext}",
|
||||
}
|
||||
break
|
||||
for ext in exts:
|
||||
p = signals_dir / f"turn_signal_blindspot{ext}"
|
||||
if p.exists():
|
||||
response_data["images"]["turnSignalBlindspot"] = {
|
||||
"filename": f"turn_signal_blindspot{ext}",
|
||||
"path": f"signals/turn_signal_blindspot{ext}",
|
||||
}
|
||||
break
|
||||
|
||||
sounds_dir = theme_dir / "sounds"
|
||||
if sounds_dir.exists():
|
||||
for name in ["engage", "disengage", "startup", "prompt"]:
|
||||
file_path = sounds_dir / f"{name}.wav"
|
||||
if file_path.exists():
|
||||
response_data["sounds"][name] = {
|
||||
"filename": f"{name}.wav",
|
||||
"path": f"sounds/{name}.wav"
|
||||
}
|
||||
|
||||
steering_wheel_path = None
|
||||
if "holiday" in theme_type:
|
||||
steering_dir = theme_dir / "steering_wheel"
|
||||
if steering_dir.exists() and steering_dir.is_dir():
|
||||
for file in steering_dir.iterdir():
|
||||
if file.is_file() and file.suffix.lower() in [".png", ".jpg", ".jpeg", ".gif"]:
|
||||
steering_wheel_path = f"steering_wheel/{file.name}"
|
||||
break
|
||||
else:
|
||||
steering_wheels_dir = THEME_SAVE_PATH / "steering_wheels"
|
||||
if steering_wheels_dir.exists():
|
||||
for file in steering_wheels_dir.iterdir():
|
||||
if file.is_file() and file.stem.lower() == theme_path.lower() and file.suffix.lower() in [".png", ".jpg", ".jpeg", ".gif"]:
|
||||
steering_wheel_path = f"steering_wheels/{file.name}"
|
||||
break
|
||||
|
||||
if steering_wheel_path:
|
||||
response_data["images"]["steeringWheel"] = {
|
||||
"filename": steering_wheel_path.split("/")[-1],
|
||||
"path": steering_wheel_path
|
||||
}
|
||||
|
||||
return jsonify(response_data)
|
||||
|
||||
@app.route("/api/themes/submit", methods=["POST"])
|
||||
def submit_theme():
|
||||
if not GITLAB_TOKEN:
|
||||
return jsonify({"error": "Missing GitLab token"}), 500
|
||||
|
||||
try:
|
||||
theme_name = request.form.get("themeName")
|
||||
if not theme_name:
|
||||
return jsonify({"error": "Missing theme name"}), 400
|
||||
|
||||
discord_username = request.form.get("discordUsername") or "Unknown"
|
||||
|
||||
theme_path, error = utilities.create_theme(request.form, request.files, temporary=True)
|
||||
if error:
|
||||
return jsonify({"message": error}), 400
|
||||
|
||||
safe_theme_name = utilities.normalize_theme_name(theme_name, for_path=True)
|
||||
combined_name = f"{safe_theme_name}~{discord_username}"
|
||||
timestamp = int(time.time())
|
||||
|
||||
def gitlab_post(project_id, endpoint, payload):
|
||||
url = f"{GITLAB_API}/projects/{project_id}/{endpoint}"
|
||||
resp = requests.post(url, headers={"PRIVATE-TOKEN": GITLAB_TOKEN}, json=payload)
|
||||
if resp.status_code not in (200, 201):
|
||||
raise RuntimeError(f"GitLab API error {resp.status_code}: {resp.text}")
|
||||
return resp.json()
|
||||
|
||||
def encode_file_base64(path):
|
||||
with open(path, "rb") as f:
|
||||
return base64.b64encode(f.read()).decode("utf-8")
|
||||
|
||||
def send_discord_notification(username, theme_name, asset_types):
|
||||
if not DISCORD_WEBHOOK_URL:
|
||||
return
|
||||
|
||||
message = (
|
||||
f"🎨 **New Theme Submission**\n"
|
||||
f"User: `{username}`\n"
|
||||
f"Theme: `{theme_name}`\n"
|
||||
f"Assets: {', '.join(asset_types)}\n"
|
||||
f"[View Submissions Repo](https://gitlab.com/{RESOURCES_REPO}-Submissions)\n"
|
||||
f"<@263565721336807424>"
|
||||
)
|
||||
payload = {"content": message}
|
||||
try:
|
||||
resp = requests.post(DISCORD_WEBHOOK_URL, json=payload)
|
||||
if resp.status_code not in (200, 204):
|
||||
print(f"Discord notification failed: {resp.status_code} {resp.text}")
|
||||
except Exception as exception:
|
||||
print(f"Error sending Discord message: {exception}")
|
||||
|
||||
asset_types = []
|
||||
submission_urls = {}
|
||||
|
||||
distance_icons_path = theme_path / "distance_icons"
|
||||
if distance_icons_path.exists() and any(distance_icons_path.iterdir()):
|
||||
zip_path = shutil.make_archive(str(distance_icons_path), "zip", distance_icons_path)
|
||||
encoded = encode_file_base64(zip_path)
|
||||
file_name = f"{combined_name}.zip"
|
||||
actions = [
|
||||
{
|
||||
"action": "create",
|
||||
"file_path": file_name,
|
||||
"content": encoded,
|
||||
"encoding": "base64"
|
||||
}
|
||||
]
|
||||
commit_payload = {
|
||||
"branch": "Distance-Icons",
|
||||
"commit_message": f"Added Distance Icons: {combined_name}",
|
||||
"actions": actions
|
||||
}
|
||||
gitlab_post(GITLAB_SUBMISSIONS_PROJECT_ID, "repository/commits", commit_payload)
|
||||
asset_types.append("Distance Icons")
|
||||
submission_urls["distance_icons"] = f"https://gitlab.com/{RESOURCES_REPO}-Submissions/-/tree/Distance-Icons"
|
||||
|
||||
theme_actions = []
|
||||
for folder in ["colors", "icons", "signals", "sounds"]:
|
||||
folder_path = theme_path / folder
|
||||
if folder_path.exists() and any(folder_path.iterdir()):
|
||||
zip_path = shutil.make_archive(str(folder_path), "zip", folder_path)
|
||||
encoded = encode_file_base64(zip_path)
|
||||
file_path = f"{combined_name}/{folder}.zip"
|
||||
theme_actions.append({
|
||||
"action": "create",
|
||||
"file_path": file_path,
|
||||
"content": encoded,
|
||||
"encoding": "base64"
|
||||
})
|
||||
|
||||
if theme_actions:
|
||||
commit_payload = {
|
||||
"branch": "Themes",
|
||||
"commit_message": f"Added Theme: {combined_name}",
|
||||
"actions": theme_actions
|
||||
}
|
||||
gitlab_post(GITLAB_SUBMISSIONS_PROJECT_ID, "repository/commits", commit_payload)
|
||||
asset_types.append("Theme")
|
||||
submission_urls["theme"] = f"https://gitlab.com/{RESOURCES_REPO}-Submissions/-/tree/Themes"
|
||||
|
||||
wheel_file = request.files.get("steeringWheel")
|
||||
if wheel_file and wheel_file.filename:
|
||||
suffix = Path(wheel_file.filename).suffix
|
||||
file_name = f"{combined_name}{suffix}"
|
||||
wheel_file.seek(0)
|
||||
encoded_wheel = base64.b64encode(wheel_file.read()).decode("utf-8")
|
||||
actions = [
|
||||
{
|
||||
"action": "create",
|
||||
"file_path": file_name,
|
||||
"content": encoded_wheel,
|
||||
"encoding": "base64"
|
||||
}
|
||||
]
|
||||
commit_payload = {
|
||||
"branch": "Steering-Wheels",
|
||||
"commit_message": f"Added Steering Wheel: {combined_name}",
|
||||
"actions": actions
|
||||
}
|
||||
gitlab_post(GITLAB_SUBMISSIONS_PROJECT_ID, "repository/commits", commit_payload)
|
||||
asset_types.append("Steering Wheel")
|
||||
submission_urls["steering_wheel"] = f"https://gitlab.com/{RESOURCES_REPO}-Submissions/-/tree/Steering-Wheels"
|
||||
|
||||
if not submission_urls:
|
||||
return jsonify({"error": "No valid theme data or steering wheel file provided"}), 400
|
||||
|
||||
send_discord_notification(discord_username, theme_name, asset_types)
|
||||
|
||||
return jsonify({
|
||||
"message": "Submission successful!",
|
||||
"branches": submission_urls
|
||||
}), 200
|
||||
|
||||
except Exception as exception:
|
||||
return jsonify({"error": str(exception)}), 500
|
||||
|
||||
finally:
|
||||
if "theme_path" in locals() and theme_path.parent.exists():
|
||||
delete_file(theme_path.parent)
|
||||
|
||||
@app.route("/api/tmux_log/capture", methods=["POST"])
|
||||
def capture_tmux_log_route():
|
||||
TMUX_LOGS_PATH.mkdir(parents=True, exist_ok=True)
|
||||
@@ -924,16 +1573,65 @@ def setup(app):
|
||||
def get_video(path):
|
||||
camera = request.args.get("camera")
|
||||
filename = {"driver": "dcamera.hevc", "wide": "ecamera.hevc"}.get(camera, "fcamera.hevc")
|
||||
|
||||
for footage_path in FOOTAGE_PATHS:
|
||||
filepath = f"{footage_path}{path}/{filename}"
|
||||
if os.path.exists(filepath):
|
||||
process = utilities.ffmpeg_mp4_wrap_process_builder(filepath)
|
||||
return Response(process.stdout.read(), status=200, mimetype="video/mp4")
|
||||
file_handle = utilities.ffmpeg_mp4_wrap_process_builder(filepath)
|
||||
|
||||
file_handle.seek(0, 2)
|
||||
file_size = file_handle.tell()
|
||||
file_handle.seek(0)
|
||||
|
||||
range_header = request.headers.get('Range', None)
|
||||
if range_header:
|
||||
byte_start = 0
|
||||
byte_end = file_size - 1
|
||||
|
||||
if range_header.startswith('bytes='):
|
||||
range_spec = range_header[6:]
|
||||
if '-' in range_spec:
|
||||
start, end = range_spec.split('-', 1)
|
||||
if start:
|
||||
byte_start = max(0, int(start))
|
||||
if end:
|
||||
byte_end = min(file_size - 1, int(end))
|
||||
|
||||
if byte_start >= file_size:
|
||||
file_handle.close()
|
||||
return Response("Requested Range Not Satisfiable", 416)
|
||||
|
||||
byte_end = max(byte_start, byte_end)
|
||||
|
||||
file_handle.seek(byte_start)
|
||||
read_length = byte_end - byte_start + 1
|
||||
data = file_handle.read(read_length)
|
||||
|
||||
response = Response(
|
||||
data,
|
||||
206,
|
||||
headers={
|
||||
'Content-Range': f'bytes {byte_start}-{byte_end}/{file_size}',
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': str(len(data)),
|
||||
'Content-Type': 'video/mp4'
|
||||
}
|
||||
)
|
||||
else:
|
||||
data = file_handle.read()
|
||||
response = Response(
|
||||
data,
|
||||
200,
|
||||
headers={
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': str(file_size),
|
||||
'Content-Type': 'video/mp4'
|
||||
}
|
||||
)
|
||||
|
||||
file_handle.close()
|
||||
return response
|
||||
return {"error": "Video not found"}, 404
|
||||
|
||||
|
||||
def main():
|
||||
app = Flask(__name__, static_folder="assets", static_url_path="/assets")
|
||||
setup(app)
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
from pydub import AudioSegment
|
||||
from typing import List
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from openpilot.common.conversions import Conversions as CV
|
||||
from openpilot.system.loggerd.config import get_available_bytes, get_used_bytes
|
||||
@@ -17,7 +23,8 @@ from openpilot.system.loggerd.deleter import PRESERVE_ATTR_NAME, PRESERVE_ATTR_V
|
||||
from openpilot.system.loggerd.uploader import listdir_by_creation
|
||||
from openpilot.tools.lib.route import SegmentName
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_variables import params, params_tracking
|
||||
from openpilot.frogpilot.common.frogpilot_variables import THEME_SAVE_PATH, VIDEO_CACHE_PATH, params
|
||||
from openpilot.frogpilot.assets.theme_manager import HOLIDAY_THEME_PATH
|
||||
|
||||
LOG_CANDIDATES = [
|
||||
"qlog",
|
||||
@@ -29,8 +36,293 @@ LOG_CANDIDATES = [
|
||||
|
||||
SEGMENT_RE = re.compile(r"^[0-9a-fA-F]{8}--[0-9a-fA-F]{10}--\d+$")
|
||||
|
||||
TARGET_LOUDNESS = -15.0
|
||||
|
||||
XOR_KEY = "s8#pL3*Xj!aZ@dWq"
|
||||
|
||||
MAX_FILE_SIZE = 5 * 1024 * 1024
|
||||
|
||||
def check_theme_components(theme_path):
|
||||
components = {
|
||||
"hasColors": False,
|
||||
"hasIcons": False,
|
||||
"hasSounds": False,
|
||||
"hasTurnSignals": False,
|
||||
"hasDistanceIcons": False,
|
||||
"hasSteeringWheel": False
|
||||
}
|
||||
|
||||
colors_path = theme_path / "colors" / "colors.json"
|
||||
if colors_path.exists():
|
||||
components["hasColors"] = True
|
||||
|
||||
icons_path = theme_path / "icons"
|
||||
if icons_path.exists() and any(icons_path.iterdir()):
|
||||
components["hasIcons"] = True
|
||||
|
||||
sounds_path = theme_path / "sounds"
|
||||
if sounds_path.exists() and any(sounds_path.iterdir()):
|
||||
components["hasSounds"] = True
|
||||
|
||||
signals_path = theme_path / "signals"
|
||||
if signals_path.exists() and any(signals_path.iterdir()):
|
||||
components["hasTurnSignals"] = True
|
||||
|
||||
distance_icons_path = theme_path / "distance_icons"
|
||||
if distance_icons_path.exists() and any(distance_icons_path.iterdir()):
|
||||
components["hasDistanceIcons"] = True
|
||||
|
||||
is_holiday_theme = str(HOLIDAY_THEME_PATH) in str(theme_path)
|
||||
|
||||
if is_holiday_theme:
|
||||
wheel_path = theme_path / "steering_wheel"
|
||||
if wheel_path.exists() and any(f.name.startswith("wheel.") for f in wheel_path.iterdir()):
|
||||
components["hasSteeringWheel"] = True
|
||||
else:
|
||||
wheel_path = THEME_SAVE_PATH / "steering_wheels"
|
||||
if wheel_path.exists():
|
||||
theme_name = theme_path.name.replace('-user_created', '')
|
||||
if any(wheel_path.glob(f"{theme_name}-user_created.*")):
|
||||
components["hasSteeringWheel"] = True
|
||||
|
||||
return components
|
||||
|
||||
def covert_audio(input_file):
|
||||
sound = AudioSegment.from_file(input_file)
|
||||
sound = sound.set_frame_rate(48000)
|
||||
sound = sound.set_channels(1)
|
||||
|
||||
output_filename = os.path.splitext(input_file)[0] + ".wav"
|
||||
sound.export(output_filename, format="wav", parameters=["-acodec", "pcm_s16le"])
|
||||
|
||||
if input_file != output_filename:
|
||||
os.remove(input_file)
|
||||
|
||||
def create_theme(form_data, files, temporary=False):
|
||||
theme_name = form_data.get("themeName")
|
||||
if not theme_name:
|
||||
return None, "Theme name is required."
|
||||
|
||||
sane_theme_name = secure_filename(theme_name.replace(" ", "_"))
|
||||
|
||||
save_checklist_str = form_data.get("saveChecklist", "{}")
|
||||
save_checklist = json.loads(save_checklist_str)
|
||||
|
||||
needs_theme_pack = any([
|
||||
save_checklist.get("colors"),
|
||||
save_checklist.get("icons"),
|
||||
save_checklist.get("sounds"),
|
||||
save_checklist.get("turn_signals"),
|
||||
save_checklist.get("distance_icons"),
|
||||
])
|
||||
|
||||
if temporary:
|
||||
base_path = Path(f"/tmp/{sane_theme_name}_{secrets.token_hex(8)}")
|
||||
else:
|
||||
base_path = THEME_SAVE_PATH / "theme_packs" if needs_theme_pack else None
|
||||
|
||||
theme_path = (base_path / f"{sane_theme_name}-user_created") if base_path else None
|
||||
if theme_path:
|
||||
theme_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if save_checklist.get("colors"):
|
||||
(theme_path / "colors").mkdir(exist_ok=True)
|
||||
colors_str = form_data.get("colors")
|
||||
if colors_str:
|
||||
color_data = json.loads(colors_str)
|
||||
for key, values in color_data.items():
|
||||
if "alpha" in values:
|
||||
values["alpha"] = values.pop("alpha")
|
||||
colors_file = theme_path / "colors" / "colors.json"
|
||||
with open(colors_file, "w") as f:
|
||||
json.dump(color_data, f, indent=2)
|
||||
|
||||
if save_checklist.get("turn_signals"):
|
||||
signals_path = theme_path / "signals"
|
||||
signals_path.mkdir(exist_ok=True)
|
||||
|
||||
if turn_signal_length := form_data.get("turnSignalLength"):
|
||||
style = form_data.get("turnSignalStyle", "Traditional").lower()
|
||||
(signals_path / f"{style}_{turn_signal_length}").touch()
|
||||
|
||||
turn_signal_type = form_data.get("turnSignalType", "Single Image").lower()
|
||||
|
||||
if turn_signal_type == "single image":
|
||||
for f in signals_path.glob("turn_signal.*"):
|
||||
f.unlink()
|
||||
for f in signals_path.glob("turn_signal_blindspot.*"):
|
||||
f.unlink()
|
||||
|
||||
file = files.get("turnSignal")
|
||||
if file and file.filename:
|
||||
if file.content_length > MAX_FILE_SIZE:
|
||||
return None, f"File {file.filename} exceeds 1MB limit."
|
||||
ext = Path(file.filename).suffix
|
||||
file.save(signals_path / f"turn_signal{ext}")
|
||||
|
||||
file = files.get("turnSignalBlindspot")
|
||||
if file and file.filename:
|
||||
if file.content_length > MAX_FILE_SIZE:
|
||||
return None, f"File {file.filename} exceeds 1MB limit."
|
||||
ext = Path(file.filename).suffix
|
||||
file.save(signals_path / f"turn_signal_blindspot{ext}")
|
||||
|
||||
elif turn_signal_type == "sequential":
|
||||
for f in signals_path.glob("turn_signal_*"):
|
||||
f.unlink()
|
||||
|
||||
signal_map = {
|
||||
"turnSignal": "turn_signal",
|
||||
"turnSignalBlindspot": "turn_signal_blindspot",
|
||||
}
|
||||
for field, base_name in signal_map.items():
|
||||
file = files.get(field)
|
||||
if file and file.filename:
|
||||
if file.content_length > MAX_FILE_SIZE:
|
||||
return None, f"File {file.filename} exceeds 1MB limit."
|
||||
for f in signals_path.glob(f"{base_name}.*"):
|
||||
f.unlink()
|
||||
ext = Path(file.filename).suffix.lower()
|
||||
file.save(signals_path / f"{base_name}{ext}")
|
||||
|
||||
for f in signals_path.glob("turn_signal.*"):
|
||||
f.unlink()
|
||||
for f in signals_path.glob("turn_signal_blindspot.*"):
|
||||
f.unlink()
|
||||
|
||||
sequential_keys = sorted(
|
||||
[k for k in files if k.startswith("turn_signal_")],
|
||||
key=lambda name: int(name.split("_")[-1])
|
||||
)
|
||||
|
||||
for key in sequential_keys:
|
||||
file = files.get(key)
|
||||
if file and file.filename:
|
||||
if file.content_length > MAX_FILE_SIZE:
|
||||
return None, f"File {file.filename} exceeds 1MB limit."
|
||||
idx = key.split("_")[-1]
|
||||
ext = Path(file.filename).suffix
|
||||
file.save(signals_path / f"turn_signal_{idx}{ext}")
|
||||
|
||||
if save_checklist.get("icons"):
|
||||
(theme_path / "icons").mkdir(exist_ok=True)
|
||||
|
||||
icon_map = {
|
||||
"settingsButton": (theme_path / "icons", "button_settings", (169, 104)),
|
||||
"homeButton": (theme_path / "icons", "button_home", (250, 250)),
|
||||
}
|
||||
|
||||
for field, (dest_path, base_name, resize_dims) in icon_map.items():
|
||||
file = files.get(field)
|
||||
if file and file.filename:
|
||||
if file.content_length > MAX_FILE_SIZE:
|
||||
return None, f"File {file.filename} exceeds 1MB limit."
|
||||
|
||||
for f in dest_path.glob(f"{base_name}.*"):
|
||||
f.unlink()
|
||||
|
||||
ext = Path(file.filename).suffix.lower()
|
||||
save_path = dest_path / f"{base_name}{ext}"
|
||||
file.save(save_path)
|
||||
|
||||
if resize_dims:
|
||||
if ext == ".gif":
|
||||
width, height = resize_dims
|
||||
palette_path = save_path.with_suffix(".palette.png")
|
||||
temp_output_path = save_path.with_suffix(".resized.gif")
|
||||
subprocess.run(["ffmpeg", "-i", str(save_path), "-vf", "palettegen", "-y", str(palette_path)], check=True)
|
||||
subprocess.run(["ffmpeg", "-i", str(save_path), "-i", str(palette_path), "-lavfi", f"fps=20,scale={width}:{height}:flags=lanczos[x];[x][1:v]paletteuse", "-y", str(temp_output_path)], check=True)
|
||||
palette_path.unlink()
|
||||
temp_output_path.rename(save_path)
|
||||
else:
|
||||
img = Image.open(save_path).resize(resize_dims, Image.Resampling.LANCZOS)
|
||||
if ext != ".png":
|
||||
save_path.unlink()
|
||||
save_path = save_path.with_suffix(".png")
|
||||
img.save(save_path, "PNG")
|
||||
|
||||
if save_checklist.get("steering_wheel"):
|
||||
wheels_dir = THEME_SAVE_PATH / "steering_wheels"
|
||||
wheels_dir.mkdir(parents=True, exist_ok=True)
|
||||
file = files.get("steeringWheel")
|
||||
saved_wheel_path = None
|
||||
if file and file.filename:
|
||||
if file.content_length > MAX_FILE_SIZE:
|
||||
return None, f"File {file.filename} exceeds 1MB limit."
|
||||
for f in wheels_dir.glob(f"{sane_theme_name}-user_created.*"):
|
||||
f.unlink()
|
||||
ext = Path(file.filename).suffix.lower()
|
||||
saved_wheel_path = wheels_dir / f"{sane_theme_name}-user_created{ext}"
|
||||
file.save(saved_wheel_path)
|
||||
if ext == ".gif":
|
||||
width, height = (250, 250)
|
||||
palette_path = saved_wheel_path.with_suffix(".palette.png")
|
||||
temp_output_path = saved_wheel_path.with_suffix(".resized.gif")
|
||||
subprocess.run(["ffmpeg", "-i", str(saved_wheel_path), "-vf", "palettegen", "-y", str(palette_path)], check=True)
|
||||
subprocess.run(["ffmpeg", "-i", str(saved_wheel_path), "-i", str(palette_path), "-lavfi", f"fps=20,scale={width}:{height}:flags=lanczos[x];[x][1:v]paletteuse", "-y", str(temp_output_path)], check=True)
|
||||
palette_path.unlink()
|
||||
temp_output_path.rename(saved_wheel_path)
|
||||
else:
|
||||
img = Image.open(saved_wheel_path).resize((250, 250), Image.Resampling.LANCZOS)
|
||||
if ext != ".png":
|
||||
saved_wheel_path.unlink()
|
||||
saved_wheel_path = saved_wheel_path.with_suffix(".png")
|
||||
img.save(saved_wheel_path, "PNG")
|
||||
if temporary and (theme_path is not None):
|
||||
existing = saved_wheel_path if saved_wheel_path is not None else next(wheels_dir.glob(f"{sane_theme_name}-user_created.*"), None)
|
||||
if existing:
|
||||
wheel_icon_dir = theme_path / "WheelIcon"
|
||||
wheel_icon_dir.mkdir(parents=True, exist_ok=True)
|
||||
dest = wheel_icon_dir / f"wheel{existing.suffix.lower()}"
|
||||
if dest.exists():
|
||||
dest.unlink()
|
||||
dest.symlink_to(existing)
|
||||
|
||||
if save_checklist.get("distance_icons"):
|
||||
dist_path = theme_path / "distance_icons"
|
||||
dist_path.mkdir(exist_ok=True)
|
||||
for name in ["traffic", "aggressive", "standard", "relaxed"]:
|
||||
file = files.get(f"distanceIcons_{name}")
|
||||
if file and file.filename:
|
||||
if file.content_length > MAX_FILE_SIZE:
|
||||
return None, f"File {file.filename} exceeds 1MB limit."
|
||||
|
||||
for f in dist_path.glob(f"{name}.*"):
|
||||
f.unlink()
|
||||
|
||||
ext = Path(file.filename).suffix.lower()
|
||||
save_path = dist_path / f"{name}{ext}"
|
||||
file.save(save_path)
|
||||
if ext == ".gif":
|
||||
width, height = (250, 250)
|
||||
palette_path = save_path.with_suffix(".palette.png")
|
||||
temp_output_path = save_path.with_suffix(".resized.gif")
|
||||
subprocess.run(["ffmpeg", "-i", str(save_path), "-vf", "palettegen", "-y", str(palette_path)], check=True)
|
||||
subprocess.run(["ffmpeg", "-i", str(save_path), "-i", str(palette_path), "-lavfi", f"fps=20,scale={width}:{height}:flags=lanczos[x];[x][1:v]paletteuse", "-y", str(temp_output_path)], check=True)
|
||||
palette_path.unlink()
|
||||
temp_output_path.rename(save_path)
|
||||
else:
|
||||
img = Image.open(save_path).resize((250, 250), Image.Resampling.LANCZOS)
|
||||
if ext != ".png":
|
||||
save_path.unlink()
|
||||
save_path = save_path.with_suffix(".png")
|
||||
img.save(save_path, "PNG")
|
||||
|
||||
if save_checklist.get("sounds"):
|
||||
sounds_path = theme_path / "sounds"
|
||||
sounds_path.mkdir(exist_ok=True)
|
||||
for name in ["engage", "disengage", "prompt", "startup"]:
|
||||
file = files.get(name)
|
||||
if file and file.filename:
|
||||
if file.content_length > MAX_FILE_SIZE:
|
||||
return None, f"File {file.filename} exceeds 1MB limit."
|
||||
|
||||
save_path = sounds_path / f"{name}{Path(file.filename).suffix}"
|
||||
file.save(save_path)
|
||||
covert_audio(str(save_path))
|
||||
|
||||
return theme_path, None
|
||||
|
||||
def decode_parameters(encoded_string):
|
||||
obfuscated_data = base64.b64decode(encoded_string.encode("utf-8")).decode("utf-8")
|
||||
decrypted_data = xor_encrypt_decrypt(obfuscated_data, XOR_KEY)
|
||||
@@ -42,26 +334,89 @@ def encode_parameters(params_dict):
|
||||
encoded_data = base64.b64encode(obfuscated_data.encode("utf-8")).decode("utf-8")
|
||||
return encoded_data
|
||||
|
||||
def ffmpeg_concat_segments_to_mp4(input_files, cache_key=None):
|
||||
if not input_files:
|
||||
raise ValueError("No input files provided for concatenation")
|
||||
|
||||
VIDEO_CACHE_PATH.mkdir(exist_ok=True)
|
||||
|
||||
key_str = "|".join(str(p) for p in input_files)
|
||||
if cache_key:
|
||||
key_str = f"{cache_key}|{key_str}"
|
||||
file_hash = hashlib.md5(key_str.encode()).hexdigest()
|
||||
cache_path = VIDEO_CACHE_PATH / f"{file_hash}.mp4"
|
||||
|
||||
if cache_path.exists() and all(cache_path.stat().st_mtime > Path(f).stat().st_mtime for f in input_files):
|
||||
return open(cache_path, "rb")
|
||||
|
||||
list_file = VIDEO_CACHE_PATH / f"{file_hash}.txt"
|
||||
with open(list_file, "w") as f:
|
||||
for seg in input_files:
|
||||
f.write(f"file '{Path(seg)}'\n")
|
||||
|
||||
try:
|
||||
subprocess.run(
|
||||
["ffmpeg", "-hide_banner", "-loglevel", "error", "-f", "concat", "-safe", "0",
|
||||
"-i", str(list_file), "-c", "copy", "-movflags", "faststart", "-y", str(cache_path)],
|
||||
check=True
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
try:
|
||||
subprocess.run(
|
||||
["ffmpeg", "-hide_banner", "-loglevel", "error", "-f", "concat", "-safe", "0",
|
||||
"-i", str(list_file), "-c:v", "libx264", "-movflags", "faststart", "-y", str(cache_path)],
|
||||
check=True
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
if cache_path.exists():
|
||||
cache_path.unlink()
|
||||
raise ValueError(f"Cannot process concatenated video segments: {input_files}")
|
||||
finally:
|
||||
if list_file.exists():
|
||||
list_file.unlink()
|
||||
|
||||
return open(cache_path, "rb")
|
||||
|
||||
def ffmpeg_mp4_wrap_process_builder(filename):
|
||||
is_raw_hevc = filename.rsplit(".", 1)[-1] == "hevc"
|
||||
input_path = Path(filename)
|
||||
|
||||
command = [
|
||||
"ffmpeg",
|
||||
"-hide_banner",
|
||||
"-loglevel", "error",
|
||||
"-probesize", "1M",
|
||||
"-analyzeduration", "1M",
|
||||
*(["-f", "hevc"] if is_raw_hevc else []),
|
||||
"-i", filename,
|
||||
"-c", "copy",
|
||||
"-map", "0",
|
||||
*(["-vtag", "hvc1"] if is_raw_hevc else []),
|
||||
"-f", "mp4",
|
||||
"-movflags", "empty_moov",
|
||||
"-"
|
||||
]
|
||||
if not input_path.exists():
|
||||
raise FileNotFoundError(f"Input file does not exist: {input_path}")
|
||||
|
||||
return subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
if input_path.stat().st_size == 0:
|
||||
raise ValueError(f"Input file is empty: {input_path}")
|
||||
|
||||
lock_file = input_path.parent / "rlog.lock"
|
||||
if lock_file.exists():
|
||||
raise ValueError(f"File is still being recorded: {input_path}")
|
||||
|
||||
VIDEO_CACHE_PATH.mkdir(exist_ok=True)
|
||||
|
||||
total, used, free = shutil.disk_usage(VIDEO_CACHE_PATH)
|
||||
if free < 500 * 1024 * 1024:
|
||||
for cache_file in VIDEO_CACHE_PATH.glob("*.mp4"):
|
||||
try:
|
||||
cache_file.unlink()
|
||||
except:
|
||||
pass
|
||||
|
||||
file_hash = hashlib.md5(str(input_path).encode()).hexdigest()
|
||||
cache_path = VIDEO_CACHE_PATH / f"{file_hash}.mp4"
|
||||
|
||||
if cache_path.exists() and cache_path.stat().st_mtime > input_path.stat().st_mtime:
|
||||
return open(cache_path, "rb")
|
||||
|
||||
try:
|
||||
subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", "-i", str(input_path), "-c", "copy", "-movflags", "faststart", "-y", str(cache_path)], check=True)
|
||||
except subprocess.CalledProcessError:
|
||||
try:
|
||||
subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", "-i", str(input_path), "-c:v", "libx264", "-movflags", "faststart", "-y", str(cache_path)], check=True)
|
||||
except subprocess.CalledProcessError:
|
||||
if cache_path.exists():
|
||||
cache_path.unlink()
|
||||
raise ValueError(f"Cannot process video file: {input_path}")
|
||||
|
||||
return open(cache_path, "rb")
|
||||
|
||||
def format_git_date(raw_date: str):
|
||||
date_object = datetime.strptime(raw_date.split()[1], "%Y-%m-%d")
|
||||
@@ -109,15 +464,15 @@ def get_disk_usage():
|
||||
|
||||
def get_drive_stats():
|
||||
stats = json.loads(params.get("ApiCache_DriveStats", encoding="utf-8") or "{}")
|
||||
frogpilot_stats = json.loads(params.get("FrogPilotStats") or "{}")
|
||||
|
||||
is_metric = params.get_bool("IsMetric")
|
||||
conversion = 1 if is_metric else CV.KPH_TO_MPH
|
||||
unit = "kilometers" if is_metric else "miles"
|
||||
|
||||
def process(timeframe):
|
||||
data = stats.get(timeframe, {})
|
||||
return {
|
||||
"distance": data.get("distance", 0) * conversion,
|
||||
"distance": data.get("distance", 0) * (1 if is_metric else CV.KPH_TO_MPH),
|
||||
"drives": data.get("routes", 0),
|
||||
"hours": data.get("minutes", 0) / 60,
|
||||
"unit": unit
|
||||
@@ -126,9 +481,9 @@ def get_drive_stats():
|
||||
stats["all"] = process("all")
|
||||
stats["week"] = process("week")
|
||||
stats["frogpilot"] = {
|
||||
"distance": params_tracking.get_int("FrogPilotKilometers") * conversion,
|
||||
"hours": params_tracking.get_int("FrogPilotMinutes") / 60,
|
||||
"drives": params_tracking.get_int("FrogPilotDrives"),
|
||||
"distance": frogpilot_stats.get("FrogPilotMeters", 0) * (0.001 if is_metric else CV.METER_TO_MILE),
|
||||
"hours": frogpilot_stats.get("FrogPilotSeconds", 0) / (60 * 60),
|
||||
"drives": frogpilot_stats.get("FrogPilotDrives", 0),
|
||||
"unit": unit
|
||||
}
|
||||
|
||||
@@ -173,6 +528,18 @@ def has_preserve_attr(path: str):
|
||||
def list_file(path):
|
||||
return sorted(os.listdir(path), reverse=True)
|
||||
|
||||
def normalize_theme_name(name, for_path=False):
|
||||
name = name.replace("-user_created", "")
|
||||
if for_path:
|
||||
return name.lower().replace(" (", "-").replace(")", "").replace(" ", "-").replace("'", "").replace(".", "")
|
||||
|
||||
parts = re.split(r'[-_]', name)
|
||||
normalized_parts = [part.capitalize() for part in parts]
|
||||
|
||||
if '-' in name and len(normalized_parts) > 1:
|
||||
return f"{normalized_parts[0]} ({' '.join(normalized_parts[1:])})".replace(" Week", "")
|
||||
return ' '.join(normalized_parts).replace(" Week", "")
|
||||
|
||||
def process_route(footage_path, route_name):
|
||||
segment_path = f"{footage_path}{route_name}--0"
|
||||
qcamera_path = f"{segment_path}/qcamera.ts"
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
from .core import contents, where
|
||||
|
||||
__all__ = ["contents", "where"]
|
||||
__version__ = "2025.08.03"
|
||||
@@ -0,0 +1,12 @@
|
||||
import argparse
|
||||
|
||||
from certifi import contents, where
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("-c", "--contents", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.contents:
|
||||
print(contents())
|
||||
else:
|
||||
print(where())
|
||||
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
certifi.py
|
||||
~~~~~~~~~~
|
||||
|
||||
This module returns the installation location of cacert.pem or its contents.
|
||||
"""
|
||||
import sys
|
||||
import atexit
|
||||
|
||||
def exit_cacert_ctx() -> None:
|
||||
_CACERT_CTX.__exit__(None, None, None) # type: ignore[union-attr]
|
||||
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
|
||||
from importlib.resources import as_file, files
|
||||
|
||||
_CACERT_CTX = None
|
||||
_CACERT_PATH = None
|
||||
|
||||
def where() -> str:
|
||||
# This is slightly terrible, but we want to delay extracting the file
|
||||
# in cases where we're inside of a zipimport situation until someone
|
||||
# actually calls where(), but we don't want to re-extract the file
|
||||
# on every call of where(), so we'll do it once then store it in a
|
||||
# global variable.
|
||||
global _CACERT_CTX
|
||||
global _CACERT_PATH
|
||||
if _CACERT_PATH is None:
|
||||
# This is slightly janky, the importlib.resources API wants you to
|
||||
# manage the cleanup of this file, so it doesn't actually return a
|
||||
# path, it returns a context manager that will give you the path
|
||||
# when you enter it and will do any cleanup when you leave it. In
|
||||
# the common case of not needing a temporary file, it will just
|
||||
# return the file system location and the __exit__() is a no-op.
|
||||
#
|
||||
# We also have to hold onto the actual context manager, because
|
||||
# it will do the cleanup whenever it gets garbage collected, so
|
||||
# we will also store that at the global level as well.
|
||||
_CACERT_CTX = as_file(files("certifi").joinpath("cacert.pem"))
|
||||
_CACERT_PATH = str(_CACERT_CTX.__enter__())
|
||||
atexit.register(exit_cacert_ctx)
|
||||
|
||||
return _CACERT_PATH
|
||||
|
||||
def contents() -> str:
|
||||
return files("certifi").joinpath("cacert.pem").read_text(encoding="ascii")
|
||||
|
||||
else:
|
||||
|
||||
from importlib.resources import path as get_path, read_text
|
||||
|
||||
_CACERT_CTX = None
|
||||
_CACERT_PATH = None
|
||||
|
||||
def where() -> str:
|
||||
# This is slightly terrible, but we want to delay extracting the
|
||||
# file in cases where we're inside of a zipimport situation until
|
||||
# someone actually calls where(), but we don't want to re-extract
|
||||
# the file on every call of where(), so we'll do it once then store
|
||||
# it in a global variable.
|
||||
global _CACERT_CTX
|
||||
global _CACERT_PATH
|
||||
if _CACERT_PATH is None:
|
||||
# This is slightly janky, the importlib.resources API wants you
|
||||
# to manage the cleanup of this file, so it doesn't actually
|
||||
# return a path, it returns a context manager that will give
|
||||
# you the path when you enter it and will do any cleanup when
|
||||
# you leave it. In the common case of not needing a temporary
|
||||
# file, it will just return the file system location and the
|
||||
# __exit__() is a no-op.
|
||||
#
|
||||
# We also have to hold onto the actual context manager, because
|
||||
# it will do the cleanup whenever it gets garbage collected, so
|
||||
# we will also store that at the global level as well.
|
||||
_CACERT_CTX = get_path("certifi", "cacert.pem")
|
||||
_CACERT_PATH = str(_CACERT_CTX.__enter__())
|
||||
atexit.register(exit_cacert_ctx)
|
||||
|
||||
return _CACERT_PATH
|
||||
|
||||
def contents() -> str:
|
||||
return read_text("certifi", "cacert.pem", encoding="ascii")
|
||||
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
|
||||
try:
|
||||
from ._version import version as __version__
|
||||
except ImportError:
|
||||
__version__ = 'unknown'
|
||||
|
||||
__all__ = ['easter', 'parser', 'relativedelta', 'rrule', 'tz',
|
||||
'utils', 'zoneinfo']
|
||||
|
||||
def __getattr__(name):
|
||||
import importlib
|
||||
|
||||
if name in __all__:
|
||||
return importlib.import_module("." + name, __name__)
|
||||
raise AttributeError(
|
||||
"module {!r} has not attribute {!r}".format(__name__, name)
|
||||
)
|
||||
|
||||
|
||||
def __dir__():
|
||||
# __dir__ should include all the lazy-importable modules as well.
|
||||
return [x for x in globals() if x not in sys.modules] + __all__
|
||||
@@ -0,0 +1,43 @@
|
||||
"""
|
||||
Common code used in multiple modules.
|
||||
"""
|
||||
|
||||
|
||||
class weekday(object):
|
||||
__slots__ = ["weekday", "n"]
|
||||
|
||||
def __init__(self, weekday, n=None):
|
||||
self.weekday = weekday
|
||||
self.n = n
|
||||
|
||||
def __call__(self, n):
|
||||
if n == self.n:
|
||||
return self
|
||||
else:
|
||||
return self.__class__(self.weekday, n)
|
||||
|
||||
def __eq__(self, other):
|
||||
try:
|
||||
if self.weekday != other.weekday or self.n != other.n:
|
||||
return False
|
||||
except AttributeError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __hash__(self):
|
||||
return hash((
|
||||
self.weekday,
|
||||
self.n,
|
||||
))
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (self == other)
|
||||
|
||||
def __repr__(self):
|
||||
s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday]
|
||||
if not self.n:
|
||||
return s
|
||||
else:
|
||||
return "%s(%+d)" % (s, self.n)
|
||||
|
||||
# vim:ts=4:sw=4:et
|
||||
@@ -0,0 +1,4 @@
|
||||
# file generated by setuptools_scm
|
||||
# don't change, don't track in version control
|
||||
__version__ = version = '2.9.0.post0'
|
||||
__version_tuple__ = version_tuple = (2, 9, 0)
|
||||
@@ -0,0 +1,89 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
This module offers a generic Easter computing method for any given year, using
|
||||
Western, Orthodox or Julian algorithms.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
__all__ = ["easter", "EASTER_JULIAN", "EASTER_ORTHODOX", "EASTER_WESTERN"]
|
||||
|
||||
EASTER_JULIAN = 1
|
||||
EASTER_ORTHODOX = 2
|
||||
EASTER_WESTERN = 3
|
||||
|
||||
|
||||
def easter(year, method=EASTER_WESTERN):
|
||||
"""
|
||||
This method was ported from the work done by GM Arts,
|
||||
on top of the algorithm by Claus Tondering, which was
|
||||
based in part on the algorithm of Ouding (1940), as
|
||||
quoted in "Explanatory Supplement to the Astronomical
|
||||
Almanac", P. Kenneth Seidelmann, editor.
|
||||
|
||||
This algorithm implements three different Easter
|
||||
calculation methods:
|
||||
|
||||
1. Original calculation in Julian calendar, valid in
|
||||
dates after 326 AD
|
||||
2. Original method, with date converted to Gregorian
|
||||
calendar, valid in years 1583 to 4099
|
||||
3. Revised method, in Gregorian calendar, valid in
|
||||
years 1583 to 4099 as well
|
||||
|
||||
These methods are represented by the constants:
|
||||
|
||||
* ``EASTER_JULIAN = 1``
|
||||
* ``EASTER_ORTHODOX = 2``
|
||||
* ``EASTER_WESTERN = 3``
|
||||
|
||||
The default method is method 3.
|
||||
|
||||
More about the algorithm may be found at:
|
||||
|
||||
`GM Arts: Easter Algorithms <http://www.gmarts.org/index.php?go=415>`_
|
||||
|
||||
and
|
||||
|
||||
`The Calendar FAQ: Easter <https://www.tondering.dk/claus/cal/easter.php>`_
|
||||
|
||||
"""
|
||||
|
||||
if not (1 <= method <= 3):
|
||||
raise ValueError("invalid method")
|
||||
|
||||
# g - Golden year - 1
|
||||
# c - Century
|
||||
# h - (23 - Epact) mod 30
|
||||
# i - Number of days from March 21 to Paschal Full Moon
|
||||
# j - Weekday for PFM (0=Sunday, etc)
|
||||
# p - Number of days from March 21 to Sunday on or before PFM
|
||||
# (-6 to 28 methods 1 & 3, to 56 for method 2)
|
||||
# e - Extra days to add for method 2 (converting Julian
|
||||
# date to Gregorian date)
|
||||
|
||||
y = year
|
||||
g = y % 19
|
||||
e = 0
|
||||
if method < 3:
|
||||
# Old method
|
||||
i = (19*g + 15) % 30
|
||||
j = (y + y//4 + i) % 7
|
||||
if method == 2:
|
||||
# Extra dates to convert Julian to Gregorian date
|
||||
e = 10
|
||||
if y > 1600:
|
||||
e = e + y//100 - 16 - (y//100 - 16)//4
|
||||
else:
|
||||
# New method
|
||||
c = y//100
|
||||
h = (c - c//4 - (8*c + 13)//25 + 19*g + 15) % 30
|
||||
i = h - (h//28)*(1 - (h//28)*(29//(h + 1))*((21 - g)//11))
|
||||
j = (y + y//4 + i + 2 - c + c//4) % 7
|
||||
|
||||
# p can be from -6 to 56 corresponding to dates 22 March to 23 May
|
||||
# (later dates apply to method 2, although 23 May never actually occurs)
|
||||
p = i - j + e
|
||||
d = 1 + (p + 27 + (p + 6)//40) % 31
|
||||
m = 3 + (p + 26)//30
|
||||
return datetime.date(int(y), int(m), int(d))
|
||||
@@ -0,0 +1,61 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from ._parser import parse, parser, parserinfo, ParserError
|
||||
from ._parser import DEFAULTPARSER, DEFAULTTZPARSER
|
||||
from ._parser import UnknownTimezoneWarning
|
||||
|
||||
from ._parser import __doc__
|
||||
|
||||
from .isoparser import isoparser, isoparse
|
||||
|
||||
__all__ = ['parse', 'parser', 'parserinfo',
|
||||
'isoparse', 'isoparser',
|
||||
'ParserError',
|
||||
'UnknownTimezoneWarning']
|
||||
|
||||
|
||||
###
|
||||
# Deprecate portions of the private interface so that downstream code that
|
||||
# is improperly relying on it is given *some* notice.
|
||||
|
||||
|
||||
def __deprecated_private_func(f):
|
||||
from functools import wraps
|
||||
import warnings
|
||||
|
||||
msg = ('{name} is a private function and may break without warning, '
|
||||
'it will be moved and or renamed in future versions.')
|
||||
msg = msg.format(name=f.__name__)
|
||||
|
||||
@wraps(f)
|
||||
def deprecated_func(*args, **kwargs):
|
||||
warnings.warn(msg, DeprecationWarning)
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return deprecated_func
|
||||
|
||||
def __deprecate_private_class(c):
|
||||
import warnings
|
||||
|
||||
msg = ('{name} is a private class and may break without warning, '
|
||||
'it will be moved and or renamed in future versions.')
|
||||
msg = msg.format(name=c.__name__)
|
||||
|
||||
class private_class(c):
|
||||
__doc__ = c.__doc__
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
warnings.warn(msg, DeprecationWarning)
|
||||
super(private_class, self).__init__(*args, **kwargs)
|
||||
|
||||
private_class.__name__ = c.__name__
|
||||
|
||||
return private_class
|
||||
|
||||
|
||||
from ._parser import _timelex, _resultbase
|
||||
from ._parser import _tzparser, _parsetz
|
||||
|
||||
_timelex = __deprecate_private_class(_timelex)
|
||||
_tzparser = __deprecate_private_class(_tzparser)
|
||||
_resultbase = __deprecate_private_class(_resultbase)
|
||||
_parsetz = __deprecated_private_func(_parsetz)
|
||||
@@ -0,0 +1,416 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
This module offers a parser for ISO-8601 strings
|
||||
|
||||
It is intended to support all valid date, time and datetime formats per the
|
||||
ISO-8601 specification.
|
||||
|
||||
..versionadded:: 2.7.0
|
||||
"""
|
||||
from datetime import datetime, timedelta, time, date
|
||||
import calendar
|
||||
from dateutil import tz
|
||||
|
||||
from functools import wraps
|
||||
|
||||
import re
|
||||
import six
|
||||
|
||||
__all__ = ["isoparse", "isoparser"]
|
||||
|
||||
|
||||
def _takes_ascii(f):
|
||||
@wraps(f)
|
||||
def func(self, str_in, *args, **kwargs):
|
||||
# If it's a stream, read the whole thing
|
||||
str_in = getattr(str_in, 'read', lambda: str_in)()
|
||||
|
||||
# If it's unicode, turn it into bytes, since ISO-8601 only covers ASCII
|
||||
if isinstance(str_in, six.text_type):
|
||||
# ASCII is the same in UTF-8
|
||||
try:
|
||||
str_in = str_in.encode('ascii')
|
||||
except UnicodeEncodeError as e:
|
||||
msg = 'ISO-8601 strings should contain only ASCII characters'
|
||||
six.raise_from(ValueError(msg), e)
|
||||
|
||||
return f(self, str_in, *args, **kwargs)
|
||||
|
||||
return func
|
||||
|
||||
|
||||
class isoparser(object):
|
||||
def __init__(self, sep=None):
|
||||
"""
|
||||
:param sep:
|
||||
A single character that separates date and time portions. If
|
||||
``None``, the parser will accept any single character.
|
||||
For strict ISO-8601 adherence, pass ``'T'``.
|
||||
"""
|
||||
if sep is not None:
|
||||
if (len(sep) != 1 or ord(sep) >= 128 or sep in '0123456789'):
|
||||
raise ValueError('Separator must be a single, non-numeric ' +
|
||||
'ASCII character')
|
||||
|
||||
sep = sep.encode('ascii')
|
||||
|
||||
self._sep = sep
|
||||
|
||||
@_takes_ascii
|
||||
def isoparse(self, dt_str):
|
||||
"""
|
||||
Parse an ISO-8601 datetime string into a :class:`datetime.datetime`.
|
||||
|
||||
An ISO-8601 datetime string consists of a date portion, followed
|
||||
optionally by a time portion - the date and time portions are separated
|
||||
by a single character separator, which is ``T`` in the official
|
||||
standard. Incomplete date formats (such as ``YYYY-MM``) may *not* be
|
||||
combined with a time portion.
|
||||
|
||||
Supported date formats are:
|
||||
|
||||
Common:
|
||||
|
||||
- ``YYYY``
|
||||
- ``YYYY-MM``
|
||||
- ``YYYY-MM-DD`` or ``YYYYMMDD``
|
||||
|
||||
Uncommon:
|
||||
|
||||
- ``YYYY-Www`` or ``YYYYWww`` - ISO week (day defaults to 0)
|
||||
- ``YYYY-Www-D`` or ``YYYYWwwD`` - ISO week and day
|
||||
|
||||
The ISO week and day numbering follows the same logic as
|
||||
:func:`datetime.date.isocalendar`.
|
||||
|
||||
Supported time formats are:
|
||||
|
||||
- ``hh``
|
||||
- ``hh:mm`` or ``hhmm``
|
||||
- ``hh:mm:ss`` or ``hhmmss``
|
||||
- ``hh:mm:ss.ssssss`` (Up to 6 sub-second digits)
|
||||
|
||||
Midnight is a special case for `hh`, as the standard supports both
|
||||
00:00 and 24:00 as a representation. The decimal separator can be
|
||||
either a dot or a comma.
|
||||
|
||||
|
||||
.. caution::
|
||||
|
||||
Support for fractional components other than seconds is part of the
|
||||
ISO-8601 standard, but is not currently implemented in this parser.
|
||||
|
||||
Supported time zone offset formats are:
|
||||
|
||||
- `Z` (UTC)
|
||||
- `±HH:MM`
|
||||
- `±HHMM`
|
||||
- `±HH`
|
||||
|
||||
Offsets will be represented as :class:`dateutil.tz.tzoffset` objects,
|
||||
with the exception of UTC, which will be represented as
|
||||
:class:`dateutil.tz.tzutc`. Time zone offsets equivalent to UTC (such
|
||||
as `+00:00`) will also be represented as :class:`dateutil.tz.tzutc`.
|
||||
|
||||
:param dt_str:
|
||||
A string or stream containing only an ISO-8601 datetime string
|
||||
|
||||
:return:
|
||||
Returns a :class:`datetime.datetime` representing the string.
|
||||
Unspecified components default to their lowest value.
|
||||
|
||||
.. warning::
|
||||
|
||||
As of version 2.7.0, the strictness of the parser should not be
|
||||
considered a stable part of the contract. Any valid ISO-8601 string
|
||||
that parses correctly with the default settings will continue to
|
||||
parse correctly in future versions, but invalid strings that
|
||||
currently fail (e.g. ``2017-01-01T00:00+00:00:00``) are not
|
||||
guaranteed to continue failing in future versions if they encode
|
||||
a valid date.
|
||||
|
||||
.. versionadded:: 2.7.0
|
||||
"""
|
||||
components, pos = self._parse_isodate(dt_str)
|
||||
|
||||
if len(dt_str) > pos:
|
||||
if self._sep is None or dt_str[pos:pos + 1] == self._sep:
|
||||
components += self._parse_isotime(dt_str[pos + 1:])
|
||||
else:
|
||||
raise ValueError('String contains unknown ISO components')
|
||||
|
||||
if len(components) > 3 and components[3] == 24:
|
||||
components[3] = 0
|
||||
return datetime(*components) + timedelta(days=1)
|
||||
|
||||
return datetime(*components)
|
||||
|
||||
@_takes_ascii
|
||||
def parse_isodate(self, datestr):
|
||||
"""
|
||||
Parse the date portion of an ISO string.
|
||||
|
||||
:param datestr:
|
||||
The string portion of an ISO string, without a separator
|
||||
|
||||
:return:
|
||||
Returns a :class:`datetime.date` object
|
||||
"""
|
||||
components, pos = self._parse_isodate(datestr)
|
||||
if pos < len(datestr):
|
||||
raise ValueError('String contains unknown ISO ' +
|
||||
'components: {!r}'.format(datestr.decode('ascii')))
|
||||
return date(*components)
|
||||
|
||||
@_takes_ascii
|
||||
def parse_isotime(self, timestr):
|
||||
"""
|
||||
Parse the time portion of an ISO string.
|
||||
|
||||
:param timestr:
|
||||
The time portion of an ISO string, without a separator
|
||||
|
||||
:return:
|
||||
Returns a :class:`datetime.time` object
|
||||
"""
|
||||
components = self._parse_isotime(timestr)
|
||||
if components[0] == 24:
|
||||
components[0] = 0
|
||||
return time(*components)
|
||||
|
||||
@_takes_ascii
|
||||
def parse_tzstr(self, tzstr, zero_as_utc=True):
|
||||
"""
|
||||
Parse a valid ISO time zone string.
|
||||
|
||||
See :func:`isoparser.isoparse` for details on supported formats.
|
||||
|
||||
:param tzstr:
|
||||
A string representing an ISO time zone offset
|
||||
|
||||
:param zero_as_utc:
|
||||
Whether to return :class:`dateutil.tz.tzutc` for zero-offset zones
|
||||
|
||||
:return:
|
||||
Returns :class:`dateutil.tz.tzoffset` for offsets and
|
||||
:class:`dateutil.tz.tzutc` for ``Z`` and (if ``zero_as_utc`` is
|
||||
specified) offsets equivalent to UTC.
|
||||
"""
|
||||
return self._parse_tzstr(tzstr, zero_as_utc=zero_as_utc)
|
||||
|
||||
# Constants
|
||||
_DATE_SEP = b'-'
|
||||
_TIME_SEP = b':'
|
||||
_FRACTION_REGEX = re.compile(b'[\\.,]([0-9]+)')
|
||||
|
||||
def _parse_isodate(self, dt_str):
|
||||
try:
|
||||
return self._parse_isodate_common(dt_str)
|
||||
except ValueError:
|
||||
return self._parse_isodate_uncommon(dt_str)
|
||||
|
||||
def _parse_isodate_common(self, dt_str):
|
||||
len_str = len(dt_str)
|
||||
components = [1, 1, 1]
|
||||
|
||||
if len_str < 4:
|
||||
raise ValueError('ISO string too short')
|
||||
|
||||
# Year
|
||||
components[0] = int(dt_str[0:4])
|
||||
pos = 4
|
||||
if pos >= len_str:
|
||||
return components, pos
|
||||
|
||||
has_sep = dt_str[pos:pos + 1] == self._DATE_SEP
|
||||
if has_sep:
|
||||
pos += 1
|
||||
|
||||
# Month
|
||||
if len_str - pos < 2:
|
||||
raise ValueError('Invalid common month')
|
||||
|
||||
components[1] = int(dt_str[pos:pos + 2])
|
||||
pos += 2
|
||||
|
||||
if pos >= len_str:
|
||||
if has_sep:
|
||||
return components, pos
|
||||
else:
|
||||
raise ValueError('Invalid ISO format')
|
||||
|
||||
if has_sep:
|
||||
if dt_str[pos:pos + 1] != self._DATE_SEP:
|
||||
raise ValueError('Invalid separator in ISO string')
|
||||
pos += 1
|
||||
|
||||
# Day
|
||||
if len_str - pos < 2:
|
||||
raise ValueError('Invalid common day')
|
||||
components[2] = int(dt_str[pos:pos + 2])
|
||||
return components, pos + 2
|
||||
|
||||
def _parse_isodate_uncommon(self, dt_str):
|
||||
if len(dt_str) < 4:
|
||||
raise ValueError('ISO string too short')
|
||||
|
||||
# All ISO formats start with the year
|
||||
year = int(dt_str[0:4])
|
||||
|
||||
has_sep = dt_str[4:5] == self._DATE_SEP
|
||||
|
||||
pos = 4 + has_sep # Skip '-' if it's there
|
||||
if dt_str[pos:pos + 1] == b'W':
|
||||
# YYYY-?Www-?D?
|
||||
pos += 1
|
||||
weekno = int(dt_str[pos:pos + 2])
|
||||
pos += 2
|
||||
|
||||
dayno = 1
|
||||
if len(dt_str) > pos:
|
||||
if (dt_str[pos:pos + 1] == self._DATE_SEP) != has_sep:
|
||||
raise ValueError('Inconsistent use of dash separator')
|
||||
|
||||
pos += has_sep
|
||||
|
||||
dayno = int(dt_str[pos:pos + 1])
|
||||
pos += 1
|
||||
|
||||
base_date = self._calculate_weekdate(year, weekno, dayno)
|
||||
else:
|
||||
# YYYYDDD or YYYY-DDD
|
||||
if len(dt_str) - pos < 3:
|
||||
raise ValueError('Invalid ordinal day')
|
||||
|
||||
ordinal_day = int(dt_str[pos:pos + 3])
|
||||
pos += 3
|
||||
|
||||
if ordinal_day < 1 or ordinal_day > (365 + calendar.isleap(year)):
|
||||
raise ValueError('Invalid ordinal day' +
|
||||
' {} for year {}'.format(ordinal_day, year))
|
||||
|
||||
base_date = date(year, 1, 1) + timedelta(days=ordinal_day - 1)
|
||||
|
||||
components = [base_date.year, base_date.month, base_date.day]
|
||||
return components, pos
|
||||
|
||||
def _calculate_weekdate(self, year, week, day):
|
||||
"""
|
||||
Calculate the day of corresponding to the ISO year-week-day calendar.
|
||||
|
||||
This function is effectively the inverse of
|
||||
:func:`datetime.date.isocalendar`.
|
||||
|
||||
:param year:
|
||||
The year in the ISO calendar
|
||||
|
||||
:param week:
|
||||
The week in the ISO calendar - range is [1, 53]
|
||||
|
||||
:param day:
|
||||
The day in the ISO calendar - range is [1 (MON), 7 (SUN)]
|
||||
|
||||
:return:
|
||||
Returns a :class:`datetime.date`
|
||||
"""
|
||||
if not 0 < week < 54:
|
||||
raise ValueError('Invalid week: {}'.format(week))
|
||||
|
||||
if not 0 < day < 8: # Range is 1-7
|
||||
raise ValueError('Invalid weekday: {}'.format(day))
|
||||
|
||||
# Get week 1 for the specific year:
|
||||
jan_4 = date(year, 1, 4) # Week 1 always has January 4th in it
|
||||
week_1 = jan_4 - timedelta(days=jan_4.isocalendar()[2] - 1)
|
||||
|
||||
# Now add the specific number of weeks and days to get what we want
|
||||
week_offset = (week - 1) * 7 + (day - 1)
|
||||
return week_1 + timedelta(days=week_offset)
|
||||
|
||||
def _parse_isotime(self, timestr):
|
||||
len_str = len(timestr)
|
||||
components = [0, 0, 0, 0, None]
|
||||
pos = 0
|
||||
comp = -1
|
||||
|
||||
if len_str < 2:
|
||||
raise ValueError('ISO time too short')
|
||||
|
||||
has_sep = False
|
||||
|
||||
while pos < len_str and comp < 5:
|
||||
comp += 1
|
||||
|
||||
if timestr[pos:pos + 1] in b'-+Zz':
|
||||
# Detect time zone boundary
|
||||
components[-1] = self._parse_tzstr(timestr[pos:])
|
||||
pos = len_str
|
||||
break
|
||||
|
||||
if comp == 1 and timestr[pos:pos+1] == self._TIME_SEP:
|
||||
has_sep = True
|
||||
pos += 1
|
||||
elif comp == 2 and has_sep:
|
||||
if timestr[pos:pos+1] != self._TIME_SEP:
|
||||
raise ValueError('Inconsistent use of colon separator')
|
||||
pos += 1
|
||||
|
||||
if comp < 3:
|
||||
# Hour, minute, second
|
||||
components[comp] = int(timestr[pos:pos + 2])
|
||||
pos += 2
|
||||
|
||||
if comp == 3:
|
||||
# Fraction of a second
|
||||
frac = self._FRACTION_REGEX.match(timestr[pos:])
|
||||
if not frac:
|
||||
continue
|
||||
|
||||
us_str = frac.group(1)[:6] # Truncate to microseconds
|
||||
components[comp] = int(us_str) * 10**(6 - len(us_str))
|
||||
pos += len(frac.group())
|
||||
|
||||
if pos < len_str:
|
||||
raise ValueError('Unused components in ISO string')
|
||||
|
||||
if components[0] == 24:
|
||||
# Standard supports 00:00 and 24:00 as representations of midnight
|
||||
if any(component != 0 for component in components[1:4]):
|
||||
raise ValueError('Hour may only be 24 at 24:00:00.000')
|
||||
|
||||
return components
|
||||
|
||||
def _parse_tzstr(self, tzstr, zero_as_utc=True):
|
||||
if tzstr == b'Z' or tzstr == b'z':
|
||||
return tz.UTC
|
||||
|
||||
if len(tzstr) not in {3, 5, 6}:
|
||||
raise ValueError('Time zone offset must be 1, 3, 5 or 6 characters')
|
||||
|
||||
if tzstr[0:1] == b'-':
|
||||
mult = -1
|
||||
elif tzstr[0:1] == b'+':
|
||||
mult = 1
|
||||
else:
|
||||
raise ValueError('Time zone offset requires sign')
|
||||
|
||||
hours = int(tzstr[1:3])
|
||||
if len(tzstr) == 3:
|
||||
minutes = 0
|
||||
else:
|
||||
minutes = int(tzstr[(4 if tzstr[3:4] == self._TIME_SEP else 3):])
|
||||
|
||||
if zero_as_utc and hours == 0 and minutes == 0:
|
||||
return tz.UTC
|
||||
else:
|
||||
if minutes > 59:
|
||||
raise ValueError('Invalid minutes in time zone offset')
|
||||
|
||||
if hours > 23:
|
||||
raise ValueError('Invalid hours in time zone offset')
|
||||
|
||||
return tz.tzoffset(None, mult * (hours * 60 + minutes) * 60)
|
||||
|
||||
|
||||
DEFAULT_ISOPARSER = isoparser()
|
||||
isoparse = DEFAULT_ISOPARSER.isoparse
|
||||
@@ -0,0 +1,599 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
import calendar
|
||||
|
||||
import operator
|
||||
from math import copysign
|
||||
|
||||
from six import integer_types
|
||||
from warnings import warn
|
||||
|
||||
from ._common import weekday
|
||||
|
||||
MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7))
|
||||
|
||||
__all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
|
||||
|
||||
|
||||
class relativedelta(object):
|
||||
"""
|
||||
The relativedelta type is designed to be applied to an existing datetime and
|
||||
can replace specific components of that datetime, or represents an interval
|
||||
of time.
|
||||
|
||||
It is based on the specification of the excellent work done by M.-A. Lemburg
|
||||
in his
|
||||
`mx.DateTime <https://www.egenix.com/products/python/mxBase/mxDateTime/>`_ extension.
|
||||
However, notice that this type does *NOT* implement the same algorithm as
|
||||
his work. Do *NOT* expect it to behave like mx.DateTime's counterpart.
|
||||
|
||||
There are two different ways to build a relativedelta instance. The
|
||||
first one is passing it two date/datetime classes::
|
||||
|
||||
relativedelta(datetime1, datetime2)
|
||||
|
||||
The second one is passing it any number of the following keyword arguments::
|
||||
|
||||
relativedelta(arg1=x,arg2=y,arg3=z...)
|
||||
|
||||
year, month, day, hour, minute, second, microsecond:
|
||||
Absolute information (argument is singular); adding or subtracting a
|
||||
relativedelta with absolute information does not perform an arithmetic
|
||||
operation, but rather REPLACES the corresponding value in the
|
||||
original datetime with the value(s) in relativedelta.
|
||||
|
||||
years, months, weeks, days, hours, minutes, seconds, microseconds:
|
||||
Relative information, may be negative (argument is plural); adding
|
||||
or subtracting a relativedelta with relative information performs
|
||||
the corresponding arithmetic operation on the original datetime value
|
||||
with the information in the relativedelta.
|
||||
|
||||
weekday:
|
||||
One of the weekday instances (MO, TU, etc) available in the
|
||||
relativedelta module. These instances may receive a parameter N,
|
||||
specifying the Nth weekday, which could be positive or negative
|
||||
(like MO(+1) or MO(-2)). Not specifying it is the same as specifying
|
||||
+1. You can also use an integer, where 0=MO. This argument is always
|
||||
relative e.g. if the calculated date is already Monday, using MO(1)
|
||||
or MO(-1) won't change the day. To effectively make it absolute, use
|
||||
it in combination with the day argument (e.g. day=1, MO(1) for first
|
||||
Monday of the month).
|
||||
|
||||
leapdays:
|
||||
Will add given days to the date found, if year is a leap
|
||||
year, and the date found is post 28 of february.
|
||||
|
||||
yearday, nlyearday:
|
||||
Set the yearday or the non-leap year day (jump leap days).
|
||||
These are converted to day/month/leapdays information.
|
||||
|
||||
There are relative and absolute forms of the keyword
|
||||
arguments. The plural is relative, and the singular is
|
||||
absolute. For each argument in the order below, the absolute form
|
||||
is applied first (by setting each attribute to that value) and
|
||||
then the relative form (by adding the value to the attribute).
|
||||
|
||||
The order of attributes considered when this relativedelta is
|
||||
added to a datetime is:
|
||||
|
||||
1. Year
|
||||
2. Month
|
||||
3. Day
|
||||
4. Hours
|
||||
5. Minutes
|
||||
6. Seconds
|
||||
7. Microseconds
|
||||
|
||||
Finally, weekday is applied, using the rule described above.
|
||||
|
||||
For example
|
||||
|
||||
>>> from datetime import datetime
|
||||
>>> from dateutil.relativedelta import relativedelta, MO
|
||||
>>> dt = datetime(2018, 4, 9, 13, 37, 0)
|
||||
>>> delta = relativedelta(hours=25, day=1, weekday=MO(1))
|
||||
>>> dt + delta
|
||||
datetime.datetime(2018, 4, 2, 14, 37)
|
||||
|
||||
First, the day is set to 1 (the first of the month), then 25 hours
|
||||
are added, to get to the 2nd day and 14th hour, finally the
|
||||
weekday is applied, but since the 2nd is already a Monday there is
|
||||
no effect.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, dt1=None, dt2=None,
|
||||
years=0, months=0, days=0, leapdays=0, weeks=0,
|
||||
hours=0, minutes=0, seconds=0, microseconds=0,
|
||||
year=None, month=None, day=None, weekday=None,
|
||||
yearday=None, nlyearday=None,
|
||||
hour=None, minute=None, second=None, microsecond=None):
|
||||
|
||||
if dt1 and dt2:
|
||||
# datetime is a subclass of date. So both must be date
|
||||
if not (isinstance(dt1, datetime.date) and
|
||||
isinstance(dt2, datetime.date)):
|
||||
raise TypeError("relativedelta only diffs datetime/date")
|
||||
|
||||
# We allow two dates, or two datetimes, so we coerce them to be
|
||||
# of the same type
|
||||
if (isinstance(dt1, datetime.datetime) !=
|
||||
isinstance(dt2, datetime.datetime)):
|
||||
if not isinstance(dt1, datetime.datetime):
|
||||
dt1 = datetime.datetime.fromordinal(dt1.toordinal())
|
||||
elif not isinstance(dt2, datetime.datetime):
|
||||
dt2 = datetime.datetime.fromordinal(dt2.toordinal())
|
||||
|
||||
self.years = 0
|
||||
self.months = 0
|
||||
self.days = 0
|
||||
self.leapdays = 0
|
||||
self.hours = 0
|
||||
self.minutes = 0
|
||||
self.seconds = 0
|
||||
self.microseconds = 0
|
||||
self.year = None
|
||||
self.month = None
|
||||
self.day = None
|
||||
self.weekday = None
|
||||
self.hour = None
|
||||
self.minute = None
|
||||
self.second = None
|
||||
self.microsecond = None
|
||||
self._has_time = 0
|
||||
|
||||
# Get year / month delta between the two
|
||||
months = (dt1.year - dt2.year) * 12 + (dt1.month - dt2.month)
|
||||
self._set_months(months)
|
||||
|
||||
# Remove the year/month delta so the timedelta is just well-defined
|
||||
# time units (seconds, days and microseconds)
|
||||
dtm = self.__radd__(dt2)
|
||||
|
||||
# If we've overshot our target, make an adjustment
|
||||
if dt1 < dt2:
|
||||
compare = operator.gt
|
||||
increment = 1
|
||||
else:
|
||||
compare = operator.lt
|
||||
increment = -1
|
||||
|
||||
while compare(dt1, dtm):
|
||||
months += increment
|
||||
self._set_months(months)
|
||||
dtm = self.__radd__(dt2)
|
||||
|
||||
# Get the timedelta between the "months-adjusted" date and dt1
|
||||
delta = dt1 - dtm
|
||||
self.seconds = delta.seconds + delta.days * 86400
|
||||
self.microseconds = delta.microseconds
|
||||
else:
|
||||
# Check for non-integer values in integer-only quantities
|
||||
if any(x is not None and x != int(x) for x in (years, months)):
|
||||
raise ValueError("Non-integer years and months are "
|
||||
"ambiguous and not currently supported.")
|
||||
|
||||
# Relative information
|
||||
self.years = int(years)
|
||||
self.months = int(months)
|
||||
self.days = days + weeks * 7
|
||||
self.leapdays = leapdays
|
||||
self.hours = hours
|
||||
self.minutes = minutes
|
||||
self.seconds = seconds
|
||||
self.microseconds = microseconds
|
||||
|
||||
# Absolute information
|
||||
self.year = year
|
||||
self.month = month
|
||||
self.day = day
|
||||
self.hour = hour
|
||||
self.minute = minute
|
||||
self.second = second
|
||||
self.microsecond = microsecond
|
||||
|
||||
if any(x is not None and int(x) != x
|
||||
for x in (year, month, day, hour,
|
||||
minute, second, microsecond)):
|
||||
# For now we'll deprecate floats - later it'll be an error.
|
||||
warn("Non-integer value passed as absolute information. " +
|
||||
"This is not a well-defined condition and will raise " +
|
||||
"errors in future versions.", DeprecationWarning)
|
||||
|
||||
if isinstance(weekday, integer_types):
|
||||
self.weekday = weekdays[weekday]
|
||||
else:
|
||||
self.weekday = weekday
|
||||
|
||||
yday = 0
|
||||
if nlyearday:
|
||||
yday = nlyearday
|
||||
elif yearday:
|
||||
yday = yearday
|
||||
if yearday > 59:
|
||||
self.leapdays = -1
|
||||
if yday:
|
||||
ydayidx = [31, 59, 90, 120, 151, 181, 212,
|
||||
243, 273, 304, 334, 366]
|
||||
for idx, ydays in enumerate(ydayidx):
|
||||
if yday <= ydays:
|
||||
self.month = idx+1
|
||||
if idx == 0:
|
||||
self.day = yday
|
||||
else:
|
||||
self.day = yday-ydayidx[idx-1]
|
||||
break
|
||||
else:
|
||||
raise ValueError("invalid year day (%d)" % yday)
|
||||
|
||||
self._fix()
|
||||
|
||||
def _fix(self):
|
||||
if abs(self.microseconds) > 999999:
|
||||
s = _sign(self.microseconds)
|
||||
div, mod = divmod(self.microseconds * s, 1000000)
|
||||
self.microseconds = mod * s
|
||||
self.seconds += div * s
|
||||
if abs(self.seconds) > 59:
|
||||
s = _sign(self.seconds)
|
||||
div, mod = divmod(self.seconds * s, 60)
|
||||
self.seconds = mod * s
|
||||
self.minutes += div * s
|
||||
if abs(self.minutes) > 59:
|
||||
s = _sign(self.minutes)
|
||||
div, mod = divmod(self.minutes * s, 60)
|
||||
self.minutes = mod * s
|
||||
self.hours += div * s
|
||||
if abs(self.hours) > 23:
|
||||
s = _sign(self.hours)
|
||||
div, mod = divmod(self.hours * s, 24)
|
||||
self.hours = mod * s
|
||||
self.days += div * s
|
||||
if abs(self.months) > 11:
|
||||
s = _sign(self.months)
|
||||
div, mod = divmod(self.months * s, 12)
|
||||
self.months = mod * s
|
||||
self.years += div * s
|
||||
if (self.hours or self.minutes or self.seconds or self.microseconds
|
||||
or self.hour is not None or self.minute is not None or
|
||||
self.second is not None or self.microsecond is not None):
|
||||
self._has_time = 1
|
||||
else:
|
||||
self._has_time = 0
|
||||
|
||||
@property
|
||||
def weeks(self):
|
||||
return int(self.days / 7.0)
|
||||
|
||||
@weeks.setter
|
||||
def weeks(self, value):
|
||||
self.days = self.days - (self.weeks * 7) + value * 7
|
||||
|
||||
def _set_months(self, months):
|
||||
self.months = months
|
||||
if abs(self.months) > 11:
|
||||
s = _sign(self.months)
|
||||
div, mod = divmod(self.months * s, 12)
|
||||
self.months = mod * s
|
||||
self.years = div * s
|
||||
else:
|
||||
self.years = 0
|
||||
|
||||
def normalized(self):
|
||||
"""
|
||||
Return a version of this object represented entirely using integer
|
||||
values for the relative attributes.
|
||||
|
||||
>>> relativedelta(days=1.5, hours=2).normalized()
|
||||
relativedelta(days=+1, hours=+14)
|
||||
|
||||
:return:
|
||||
Returns a :class:`dateutil.relativedelta.relativedelta` object.
|
||||
"""
|
||||
# Cascade remainders down (rounding each to roughly nearest microsecond)
|
||||
days = int(self.days)
|
||||
|
||||
hours_f = round(self.hours + 24 * (self.days - days), 11)
|
||||
hours = int(hours_f)
|
||||
|
||||
minutes_f = round(self.minutes + 60 * (hours_f - hours), 10)
|
||||
minutes = int(minutes_f)
|
||||
|
||||
seconds_f = round(self.seconds + 60 * (minutes_f - minutes), 8)
|
||||
seconds = int(seconds_f)
|
||||
|
||||
microseconds = round(self.microseconds + 1e6 * (seconds_f - seconds))
|
||||
|
||||
# Constructor carries overflow back up with call to _fix()
|
||||
return self.__class__(years=self.years, months=self.months,
|
||||
days=days, hours=hours, minutes=minutes,
|
||||
seconds=seconds, microseconds=microseconds,
|
||||
leapdays=self.leapdays, year=self.year,
|
||||
month=self.month, day=self.day,
|
||||
weekday=self.weekday, hour=self.hour,
|
||||
minute=self.minute, second=self.second,
|
||||
microsecond=self.microsecond)
|
||||
|
||||
def __add__(self, other):
|
||||
if isinstance(other, relativedelta):
|
||||
return self.__class__(years=other.years + self.years,
|
||||
months=other.months + self.months,
|
||||
days=other.days + self.days,
|
||||
hours=other.hours + self.hours,
|
||||
minutes=other.minutes + self.minutes,
|
||||
seconds=other.seconds + self.seconds,
|
||||
microseconds=(other.microseconds +
|
||||
self.microseconds),
|
||||
leapdays=other.leapdays or self.leapdays,
|
||||
year=(other.year if other.year is not None
|
||||
else self.year),
|
||||
month=(other.month if other.month is not None
|
||||
else self.month),
|
||||
day=(other.day if other.day is not None
|
||||
else self.day),
|
||||
weekday=(other.weekday if other.weekday is not None
|
||||
else self.weekday),
|
||||
hour=(other.hour if other.hour is not None
|
||||
else self.hour),
|
||||
minute=(other.minute if other.minute is not None
|
||||
else self.minute),
|
||||
second=(other.second if other.second is not None
|
||||
else self.second),
|
||||
microsecond=(other.microsecond if other.microsecond
|
||||
is not None else
|
||||
self.microsecond))
|
||||
if isinstance(other, datetime.timedelta):
|
||||
return self.__class__(years=self.years,
|
||||
months=self.months,
|
||||
days=self.days + other.days,
|
||||
hours=self.hours,
|
||||
minutes=self.minutes,
|
||||
seconds=self.seconds + other.seconds,
|
||||
microseconds=self.microseconds + other.microseconds,
|
||||
leapdays=self.leapdays,
|
||||
year=self.year,
|
||||
month=self.month,
|
||||
day=self.day,
|
||||
weekday=self.weekday,
|
||||
hour=self.hour,
|
||||
minute=self.minute,
|
||||
second=self.second,
|
||||
microsecond=self.microsecond)
|
||||
if not isinstance(other, datetime.date):
|
||||
return NotImplemented
|
||||
elif self._has_time and not isinstance(other, datetime.datetime):
|
||||
other = datetime.datetime.fromordinal(other.toordinal())
|
||||
year = (self.year or other.year)+self.years
|
||||
month = self.month or other.month
|
||||
if self.months:
|
||||
assert 1 <= abs(self.months) <= 12
|
||||
month += self.months
|
||||
if month > 12:
|
||||
year += 1
|
||||
month -= 12
|
||||
elif month < 1:
|
||||
year -= 1
|
||||
month += 12
|
||||
day = min(calendar.monthrange(year, month)[1],
|
||||
self.day or other.day)
|
||||
repl = {"year": year, "month": month, "day": day}
|
||||
for attr in ["hour", "minute", "second", "microsecond"]:
|
||||
value = getattr(self, attr)
|
||||
if value is not None:
|
||||
repl[attr] = value
|
||||
days = self.days
|
||||
if self.leapdays and month > 2 and calendar.isleap(year):
|
||||
days += self.leapdays
|
||||
ret = (other.replace(**repl)
|
||||
+ datetime.timedelta(days=days,
|
||||
hours=self.hours,
|
||||
minutes=self.minutes,
|
||||
seconds=self.seconds,
|
||||
microseconds=self.microseconds))
|
||||
if self.weekday:
|
||||
weekday, nth = self.weekday.weekday, self.weekday.n or 1
|
||||
jumpdays = (abs(nth) - 1) * 7
|
||||
if nth > 0:
|
||||
jumpdays += (7 - ret.weekday() + weekday) % 7
|
||||
else:
|
||||
jumpdays += (ret.weekday() - weekday) % 7
|
||||
jumpdays *= -1
|
||||
ret += datetime.timedelta(days=jumpdays)
|
||||
return ret
|
||||
|
||||
def __radd__(self, other):
|
||||
return self.__add__(other)
|
||||
|
||||
def __rsub__(self, other):
|
||||
return self.__neg__().__radd__(other)
|
||||
|
||||
def __sub__(self, other):
|
||||
if not isinstance(other, relativedelta):
|
||||
return NotImplemented # In case the other object defines __rsub__
|
||||
return self.__class__(years=self.years - other.years,
|
||||
months=self.months - other.months,
|
||||
days=self.days - other.days,
|
||||
hours=self.hours - other.hours,
|
||||
minutes=self.minutes - other.minutes,
|
||||
seconds=self.seconds - other.seconds,
|
||||
microseconds=self.microseconds - other.microseconds,
|
||||
leapdays=self.leapdays or other.leapdays,
|
||||
year=(self.year if self.year is not None
|
||||
else other.year),
|
||||
month=(self.month if self.month is not None else
|
||||
other.month),
|
||||
day=(self.day if self.day is not None else
|
||||
other.day),
|
||||
weekday=(self.weekday if self.weekday is not None else
|
||||
other.weekday),
|
||||
hour=(self.hour if self.hour is not None else
|
||||
other.hour),
|
||||
minute=(self.minute if self.minute is not None else
|
||||
other.minute),
|
||||
second=(self.second if self.second is not None else
|
||||
other.second),
|
||||
microsecond=(self.microsecond if self.microsecond
|
||||
is not None else
|
||||
other.microsecond))
|
||||
|
||||
def __abs__(self):
|
||||
return self.__class__(years=abs(self.years),
|
||||
months=abs(self.months),
|
||||
days=abs(self.days),
|
||||
hours=abs(self.hours),
|
||||
minutes=abs(self.minutes),
|
||||
seconds=abs(self.seconds),
|
||||
microseconds=abs(self.microseconds),
|
||||
leapdays=self.leapdays,
|
||||
year=self.year,
|
||||
month=self.month,
|
||||
day=self.day,
|
||||
weekday=self.weekday,
|
||||
hour=self.hour,
|
||||
minute=self.minute,
|
||||
second=self.second,
|
||||
microsecond=self.microsecond)
|
||||
|
||||
def __neg__(self):
|
||||
return self.__class__(years=-self.years,
|
||||
months=-self.months,
|
||||
days=-self.days,
|
||||
hours=-self.hours,
|
||||
minutes=-self.minutes,
|
||||
seconds=-self.seconds,
|
||||
microseconds=-self.microseconds,
|
||||
leapdays=self.leapdays,
|
||||
year=self.year,
|
||||
month=self.month,
|
||||
day=self.day,
|
||||
weekday=self.weekday,
|
||||
hour=self.hour,
|
||||
minute=self.minute,
|
||||
second=self.second,
|
||||
microsecond=self.microsecond)
|
||||
|
||||
def __bool__(self):
|
||||
return not (not self.years and
|
||||
not self.months and
|
||||
not self.days and
|
||||
not self.hours and
|
||||
not self.minutes and
|
||||
not self.seconds and
|
||||
not self.microseconds and
|
||||
not self.leapdays and
|
||||
self.year is None and
|
||||
self.month is None and
|
||||
self.day is None and
|
||||
self.weekday is None and
|
||||
self.hour is None and
|
||||
self.minute is None and
|
||||
self.second is None and
|
||||
self.microsecond is None)
|
||||
# Compatibility with Python 2.x
|
||||
__nonzero__ = __bool__
|
||||
|
||||
def __mul__(self, other):
|
||||
try:
|
||||
f = float(other)
|
||||
except TypeError:
|
||||
return NotImplemented
|
||||
|
||||
return self.__class__(years=int(self.years * f),
|
||||
months=int(self.months * f),
|
||||
days=int(self.days * f),
|
||||
hours=int(self.hours * f),
|
||||
minutes=int(self.minutes * f),
|
||||
seconds=int(self.seconds * f),
|
||||
microseconds=int(self.microseconds * f),
|
||||
leapdays=self.leapdays,
|
||||
year=self.year,
|
||||
month=self.month,
|
||||
day=self.day,
|
||||
weekday=self.weekday,
|
||||
hour=self.hour,
|
||||
minute=self.minute,
|
||||
second=self.second,
|
||||
microsecond=self.microsecond)
|
||||
|
||||
__rmul__ = __mul__
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, relativedelta):
|
||||
return NotImplemented
|
||||
if self.weekday or other.weekday:
|
||||
if not self.weekday or not other.weekday:
|
||||
return False
|
||||
if self.weekday.weekday != other.weekday.weekday:
|
||||
return False
|
||||
n1, n2 = self.weekday.n, other.weekday.n
|
||||
if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)):
|
||||
return False
|
||||
return (self.years == other.years and
|
||||
self.months == other.months and
|
||||
self.days == other.days and
|
||||
self.hours == other.hours and
|
||||
self.minutes == other.minutes and
|
||||
self.seconds == other.seconds and
|
||||
self.microseconds == other.microseconds and
|
||||
self.leapdays == other.leapdays and
|
||||
self.year == other.year and
|
||||
self.month == other.month and
|
||||
self.day == other.day and
|
||||
self.hour == other.hour and
|
||||
self.minute == other.minute and
|
||||
self.second == other.second and
|
||||
self.microsecond == other.microsecond)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((
|
||||
self.weekday,
|
||||
self.years,
|
||||
self.months,
|
||||
self.days,
|
||||
self.hours,
|
||||
self.minutes,
|
||||
self.seconds,
|
||||
self.microseconds,
|
||||
self.leapdays,
|
||||
self.year,
|
||||
self.month,
|
||||
self.day,
|
||||
self.hour,
|
||||
self.minute,
|
||||
self.second,
|
||||
self.microsecond,
|
||||
))
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __div__(self, other):
|
||||
try:
|
||||
reciprocal = 1 / float(other)
|
||||
except TypeError:
|
||||
return NotImplemented
|
||||
|
||||
return self.__mul__(reciprocal)
|
||||
|
||||
__truediv__ = __div__
|
||||
|
||||
def __repr__(self):
|
||||
l = []
|
||||
for attr in ["years", "months", "days", "leapdays",
|
||||
"hours", "minutes", "seconds", "microseconds"]:
|
||||
value = getattr(self, attr)
|
||||
if value:
|
||||
l.append("{attr}={value:+g}".format(attr=attr, value=value))
|
||||
for attr in ["year", "month", "day", "weekday",
|
||||
"hour", "minute", "second", "microsecond"]:
|
||||
value = getattr(self, attr)
|
||||
if value is not None:
|
||||
l.append("{attr}={value}".format(attr=attr, value=repr(value)))
|
||||
return "{classname}({attrs})".format(classname=self.__class__.__name__,
|
||||
attrs=", ".join(l))
|
||||
|
||||
|
||||
def _sign(x):
|
||||
return int(copysign(1, x))
|
||||
|
||||
# vim:ts=4:sw=4:et
|
||||
@@ -0,0 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from .tz import *
|
||||
from .tz import __doc__
|
||||
|
||||
__all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange",
|
||||
"tzstr", "tzical", "tzwin", "tzwinlocal", "gettz",
|
||||
"enfold", "datetime_ambiguous", "datetime_exists",
|
||||
"resolve_imaginary", "UTC", "DeprecatedTzFormatWarning"]
|
||||
|
||||
|
||||
class DeprecatedTzFormatWarning(Warning):
|
||||
"""Warning raised when time zones are parsed from deprecated formats."""
|
||||
@@ -0,0 +1,419 @@
|
||||
from six import PY2
|
||||
|
||||
from functools import wraps
|
||||
|
||||
from datetime import datetime, timedelta, tzinfo
|
||||
|
||||
|
||||
ZERO = timedelta(0)
|
||||
|
||||
__all__ = ['tzname_in_python2', 'enfold']
|
||||
|
||||
|
||||
def tzname_in_python2(namefunc):
|
||||
"""Change unicode output into bytestrings in Python 2
|
||||
|
||||
tzname() API changed in Python 3. It used to return bytes, but was changed
|
||||
to unicode strings
|
||||
"""
|
||||
if PY2:
|
||||
@wraps(namefunc)
|
||||
def adjust_encoding(*args, **kwargs):
|
||||
name = namefunc(*args, **kwargs)
|
||||
if name is not None:
|
||||
name = name.encode()
|
||||
|
||||
return name
|
||||
|
||||
return adjust_encoding
|
||||
else:
|
||||
return namefunc
|
||||
|
||||
|
||||
# The following is adapted from Alexander Belopolsky's tz library
|
||||
# https://github.com/abalkin/tz
|
||||
if hasattr(datetime, 'fold'):
|
||||
# This is the pre-python 3.6 fold situation
|
||||
def enfold(dt, fold=1):
|
||||
"""
|
||||
Provides a unified interface for assigning the ``fold`` attribute to
|
||||
datetimes both before and after the implementation of PEP-495.
|
||||
|
||||
:param fold:
|
||||
The value for the ``fold`` attribute in the returned datetime. This
|
||||
should be either 0 or 1.
|
||||
|
||||
:return:
|
||||
Returns an object for which ``getattr(dt, 'fold', 0)`` returns
|
||||
``fold`` for all versions of Python. In versions prior to
|
||||
Python 3.6, this is a ``_DatetimeWithFold`` object, which is a
|
||||
subclass of :py:class:`datetime.datetime` with the ``fold``
|
||||
attribute added, if ``fold`` is 1.
|
||||
|
||||
.. versionadded:: 2.6.0
|
||||
"""
|
||||
return dt.replace(fold=fold)
|
||||
|
||||
else:
|
||||
class _DatetimeWithFold(datetime):
|
||||
"""
|
||||
This is a class designed to provide a PEP 495-compliant interface for
|
||||
Python versions before 3.6. It is used only for dates in a fold, so
|
||||
the ``fold`` attribute is fixed at ``1``.
|
||||
|
||||
.. versionadded:: 2.6.0
|
||||
"""
|
||||
__slots__ = ()
|
||||
|
||||
def replace(self, *args, **kwargs):
|
||||
"""
|
||||
Return a datetime with the same attributes, except for those
|
||||
attributes given new values by whichever keyword arguments are
|
||||
specified. Note that tzinfo=None can be specified to create a naive
|
||||
datetime from an aware datetime with no conversion of date and time
|
||||
data.
|
||||
|
||||
This is reimplemented in ``_DatetimeWithFold`` because pypy3 will
|
||||
return a ``datetime.datetime`` even if ``fold`` is unchanged.
|
||||
"""
|
||||
argnames = (
|
||||
'year', 'month', 'day', 'hour', 'minute', 'second',
|
||||
'microsecond', 'tzinfo'
|
||||
)
|
||||
|
||||
for arg, argname in zip(args, argnames):
|
||||
if argname in kwargs:
|
||||
raise TypeError('Duplicate argument: {}'.format(argname))
|
||||
|
||||
kwargs[argname] = arg
|
||||
|
||||
for argname in argnames:
|
||||
if argname not in kwargs:
|
||||
kwargs[argname] = getattr(self, argname)
|
||||
|
||||
dt_class = self.__class__ if kwargs.get('fold', 1) else datetime
|
||||
|
||||
return dt_class(**kwargs)
|
||||
|
||||
@property
|
||||
def fold(self):
|
||||
return 1
|
||||
|
||||
def enfold(dt, fold=1):
|
||||
"""
|
||||
Provides a unified interface for assigning the ``fold`` attribute to
|
||||
datetimes both before and after the implementation of PEP-495.
|
||||
|
||||
:param fold:
|
||||
The value for the ``fold`` attribute in the returned datetime. This
|
||||
should be either 0 or 1.
|
||||
|
||||
:return:
|
||||
Returns an object for which ``getattr(dt, 'fold', 0)`` returns
|
||||
``fold`` for all versions of Python. In versions prior to
|
||||
Python 3.6, this is a ``_DatetimeWithFold`` object, which is a
|
||||
subclass of :py:class:`datetime.datetime` with the ``fold``
|
||||
attribute added, if ``fold`` is 1.
|
||||
|
||||
.. versionadded:: 2.6.0
|
||||
"""
|
||||
if getattr(dt, 'fold', 0) == fold:
|
||||
return dt
|
||||
|
||||
args = dt.timetuple()[:6]
|
||||
args += (dt.microsecond, dt.tzinfo)
|
||||
|
||||
if fold:
|
||||
return _DatetimeWithFold(*args)
|
||||
else:
|
||||
return datetime(*args)
|
||||
|
||||
|
||||
def _validate_fromutc_inputs(f):
|
||||
"""
|
||||
The CPython version of ``fromutc`` checks that the input is a ``datetime``
|
||||
object and that ``self`` is attached as its ``tzinfo``.
|
||||
"""
|
||||
@wraps(f)
|
||||
def fromutc(self, dt):
|
||||
if not isinstance(dt, datetime):
|
||||
raise TypeError("fromutc() requires a datetime argument")
|
||||
if dt.tzinfo is not self:
|
||||
raise ValueError("dt.tzinfo is not self")
|
||||
|
||||
return f(self, dt)
|
||||
|
||||
return fromutc
|
||||
|
||||
|
||||
class _tzinfo(tzinfo):
|
||||
"""
|
||||
Base class for all ``dateutil`` ``tzinfo`` objects.
|
||||
"""
|
||||
|
||||
def is_ambiguous(self, dt):
|
||||
"""
|
||||
Whether or not the "wall time" of a given datetime is ambiguous in this
|
||||
zone.
|
||||
|
||||
:param dt:
|
||||
A :py:class:`datetime.datetime`, naive or time zone aware.
|
||||
|
||||
|
||||
:return:
|
||||
Returns ``True`` if ambiguous, ``False`` otherwise.
|
||||
|
||||
.. versionadded:: 2.6.0
|
||||
"""
|
||||
|
||||
dt = dt.replace(tzinfo=self)
|
||||
|
||||
wall_0 = enfold(dt, fold=0)
|
||||
wall_1 = enfold(dt, fold=1)
|
||||
|
||||
same_offset = wall_0.utcoffset() == wall_1.utcoffset()
|
||||
same_dt = wall_0.replace(tzinfo=None) == wall_1.replace(tzinfo=None)
|
||||
|
||||
return same_dt and not same_offset
|
||||
|
||||
def _fold_status(self, dt_utc, dt_wall):
|
||||
"""
|
||||
Determine the fold status of a "wall" datetime, given a representation
|
||||
of the same datetime as a (naive) UTC datetime. This is calculated based
|
||||
on the assumption that ``dt.utcoffset() - dt.dst()`` is constant for all
|
||||
datetimes, and that this offset is the actual number of hours separating
|
||||
``dt_utc`` and ``dt_wall``.
|
||||
|
||||
:param dt_utc:
|
||||
Representation of the datetime as UTC
|
||||
|
||||
:param dt_wall:
|
||||
Representation of the datetime as "wall time". This parameter must
|
||||
either have a `fold` attribute or have a fold-naive
|
||||
:class:`datetime.tzinfo` attached, otherwise the calculation may
|
||||
fail.
|
||||
"""
|
||||
if self.is_ambiguous(dt_wall):
|
||||
delta_wall = dt_wall - dt_utc
|
||||
_fold = int(delta_wall == (dt_utc.utcoffset() - dt_utc.dst()))
|
||||
else:
|
||||
_fold = 0
|
||||
|
||||
return _fold
|
||||
|
||||
def _fold(self, dt):
|
||||
return getattr(dt, 'fold', 0)
|
||||
|
||||
def _fromutc(self, dt):
|
||||
"""
|
||||
Given a timezone-aware datetime in a given timezone, calculates a
|
||||
timezone-aware datetime in a new timezone.
|
||||
|
||||
Since this is the one time that we *know* we have an unambiguous
|
||||
datetime object, we take this opportunity to determine whether the
|
||||
datetime is ambiguous and in a "fold" state (e.g. if it's the first
|
||||
occurrence, chronologically, of the ambiguous datetime).
|
||||
|
||||
:param dt:
|
||||
A timezone-aware :class:`datetime.datetime` object.
|
||||
"""
|
||||
|
||||
# Re-implement the algorithm from Python's datetime.py
|
||||
dtoff = dt.utcoffset()
|
||||
if dtoff is None:
|
||||
raise ValueError("fromutc() requires a non-None utcoffset() "
|
||||
"result")
|
||||
|
||||
# The original datetime.py code assumes that `dst()` defaults to
|
||||
# zero during ambiguous times. PEP 495 inverts this presumption, so
|
||||
# for pre-PEP 495 versions of python, we need to tweak the algorithm.
|
||||
dtdst = dt.dst()
|
||||
if dtdst is None:
|
||||
raise ValueError("fromutc() requires a non-None dst() result")
|
||||
delta = dtoff - dtdst
|
||||
|
||||
dt += delta
|
||||
# Set fold=1 so we can default to being in the fold for
|
||||
# ambiguous dates.
|
||||
dtdst = enfold(dt, fold=1).dst()
|
||||
if dtdst is None:
|
||||
raise ValueError("fromutc(): dt.dst gave inconsistent "
|
||||
"results; cannot convert")
|
||||
return dt + dtdst
|
||||
|
||||
@_validate_fromutc_inputs
|
||||
def fromutc(self, dt):
|
||||
"""
|
||||
Given a timezone-aware datetime in a given timezone, calculates a
|
||||
timezone-aware datetime in a new timezone.
|
||||
|
||||
Since this is the one time that we *know* we have an unambiguous
|
||||
datetime object, we take this opportunity to determine whether the
|
||||
datetime is ambiguous and in a "fold" state (e.g. if it's the first
|
||||
occurrence, chronologically, of the ambiguous datetime).
|
||||
|
||||
:param dt:
|
||||
A timezone-aware :class:`datetime.datetime` object.
|
||||
"""
|
||||
dt_wall = self._fromutc(dt)
|
||||
|
||||
# Calculate the fold status given the two datetimes.
|
||||
_fold = self._fold_status(dt, dt_wall)
|
||||
|
||||
# Set the default fold value for ambiguous dates
|
||||
return enfold(dt_wall, fold=_fold)
|
||||
|
||||
|
||||
class tzrangebase(_tzinfo):
|
||||
"""
|
||||
This is an abstract base class for time zones represented by an annual
|
||||
transition into and out of DST. Child classes should implement the following
|
||||
methods:
|
||||
|
||||
* ``__init__(self, *args, **kwargs)``
|
||||
* ``transitions(self, year)`` - this is expected to return a tuple of
|
||||
datetimes representing the DST on and off transitions in standard
|
||||
time.
|
||||
|
||||
A fully initialized ``tzrangebase`` subclass should also provide the
|
||||
following attributes:
|
||||
* ``hasdst``: Boolean whether or not the zone uses DST.
|
||||
* ``_dst_offset`` / ``_std_offset``: :class:`datetime.timedelta` objects
|
||||
representing the respective UTC offsets.
|
||||
* ``_dst_abbr`` / ``_std_abbr``: Strings representing the timezone short
|
||||
abbreviations in DST and STD, respectively.
|
||||
* ``_hasdst``: Whether or not the zone has DST.
|
||||
|
||||
.. versionadded:: 2.6.0
|
||||
"""
|
||||
def __init__(self):
|
||||
raise NotImplementedError('tzrangebase is an abstract base class')
|
||||
|
||||
def utcoffset(self, dt):
|
||||
isdst = self._isdst(dt)
|
||||
|
||||
if isdst is None:
|
||||
return None
|
||||
elif isdst:
|
||||
return self._dst_offset
|
||||
else:
|
||||
return self._std_offset
|
||||
|
||||
def dst(self, dt):
|
||||
isdst = self._isdst(dt)
|
||||
|
||||
if isdst is None:
|
||||
return None
|
||||
elif isdst:
|
||||
return self._dst_base_offset
|
||||
else:
|
||||
return ZERO
|
||||
|
||||
@tzname_in_python2
|
||||
def tzname(self, dt):
|
||||
if self._isdst(dt):
|
||||
return self._dst_abbr
|
||||
else:
|
||||
return self._std_abbr
|
||||
|
||||
def fromutc(self, dt):
|
||||
""" Given a datetime in UTC, return local time """
|
||||
if not isinstance(dt, datetime):
|
||||
raise TypeError("fromutc() requires a datetime argument")
|
||||
|
||||
if dt.tzinfo is not self:
|
||||
raise ValueError("dt.tzinfo is not self")
|
||||
|
||||
# Get transitions - if there are none, fixed offset
|
||||
transitions = self.transitions(dt.year)
|
||||
if transitions is None:
|
||||
return dt + self.utcoffset(dt)
|
||||
|
||||
# Get the transition times in UTC
|
||||
dston, dstoff = transitions
|
||||
|
||||
dston -= self._std_offset
|
||||
dstoff -= self._std_offset
|
||||
|
||||
utc_transitions = (dston, dstoff)
|
||||
dt_utc = dt.replace(tzinfo=None)
|
||||
|
||||
isdst = self._naive_isdst(dt_utc, utc_transitions)
|
||||
|
||||
if isdst:
|
||||
dt_wall = dt + self._dst_offset
|
||||
else:
|
||||
dt_wall = dt + self._std_offset
|
||||
|
||||
_fold = int(not isdst and self.is_ambiguous(dt_wall))
|
||||
|
||||
return enfold(dt_wall, fold=_fold)
|
||||
|
||||
def is_ambiguous(self, dt):
|
||||
"""
|
||||
Whether or not the "wall time" of a given datetime is ambiguous in this
|
||||
zone.
|
||||
|
||||
:param dt:
|
||||
A :py:class:`datetime.datetime`, naive or time zone aware.
|
||||
|
||||
|
||||
:return:
|
||||
Returns ``True`` if ambiguous, ``False`` otherwise.
|
||||
|
||||
.. versionadded:: 2.6.0
|
||||
"""
|
||||
if not self.hasdst:
|
||||
return False
|
||||
|
||||
start, end = self.transitions(dt.year)
|
||||
|
||||
dt = dt.replace(tzinfo=None)
|
||||
return (end <= dt < end + self._dst_base_offset)
|
||||
|
||||
def _isdst(self, dt):
|
||||
if not self.hasdst:
|
||||
return False
|
||||
elif dt is None:
|
||||
return None
|
||||
|
||||
transitions = self.transitions(dt.year)
|
||||
|
||||
if transitions is None:
|
||||
return False
|
||||
|
||||
dt = dt.replace(tzinfo=None)
|
||||
|
||||
isdst = self._naive_isdst(dt, transitions)
|
||||
|
||||
# Handle ambiguous dates
|
||||
if not isdst and self.is_ambiguous(dt):
|
||||
return not self._fold(dt)
|
||||
else:
|
||||
return isdst
|
||||
|
||||
def _naive_isdst(self, dt, transitions):
|
||||
dston, dstoff = transitions
|
||||
|
||||
dt = dt.replace(tzinfo=None)
|
||||
|
||||
if dston < dstoff:
|
||||
isdst = dston <= dt < dstoff
|
||||
else:
|
||||
isdst = not dstoff <= dt < dston
|
||||
|
||||
return isdst
|
||||
|
||||
@property
|
||||
def _dst_base_offset(self):
|
||||
return self._dst_offset - self._std_offset
|
||||
|
||||
__hash__ = None
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (self == other)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(...)" % self.__class__.__name__
|
||||
|
||||
__reduce__ = object.__reduce__
|
||||
@@ -0,0 +1,80 @@
|
||||
from datetime import timedelta
|
||||
import weakref
|
||||
from collections import OrderedDict
|
||||
|
||||
from six.moves import _thread
|
||||
|
||||
|
||||
class _TzSingleton(type):
|
||||
def __init__(cls, *args, **kwargs):
|
||||
cls.__instance = None
|
||||
super(_TzSingleton, cls).__init__(*args, **kwargs)
|
||||
|
||||
def __call__(cls):
|
||||
if cls.__instance is None:
|
||||
cls.__instance = super(_TzSingleton, cls).__call__()
|
||||
return cls.__instance
|
||||
|
||||
|
||||
class _TzFactory(type):
|
||||
def instance(cls, *args, **kwargs):
|
||||
"""Alternate constructor that returns a fresh instance"""
|
||||
return type.__call__(cls, *args, **kwargs)
|
||||
|
||||
|
||||
class _TzOffsetFactory(_TzFactory):
|
||||
def __init__(cls, *args, **kwargs):
|
||||
cls.__instances = weakref.WeakValueDictionary()
|
||||
cls.__strong_cache = OrderedDict()
|
||||
cls.__strong_cache_size = 8
|
||||
|
||||
cls._cache_lock = _thread.allocate_lock()
|
||||
|
||||
def __call__(cls, name, offset):
|
||||
if isinstance(offset, timedelta):
|
||||
key = (name, offset.total_seconds())
|
||||
else:
|
||||
key = (name, offset)
|
||||
|
||||
instance = cls.__instances.get(key, None)
|
||||
if instance is None:
|
||||
instance = cls.__instances.setdefault(key,
|
||||
cls.instance(name, offset))
|
||||
|
||||
# This lock may not be necessary in Python 3. See GH issue #901
|
||||
with cls._cache_lock:
|
||||
cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance)
|
||||
|
||||
# Remove an item if the strong cache is overpopulated
|
||||
if len(cls.__strong_cache) > cls.__strong_cache_size:
|
||||
cls.__strong_cache.popitem(last=False)
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
class _TzStrFactory(_TzFactory):
|
||||
def __init__(cls, *args, **kwargs):
|
||||
cls.__instances = weakref.WeakValueDictionary()
|
||||
cls.__strong_cache = OrderedDict()
|
||||
cls.__strong_cache_size = 8
|
||||
|
||||
cls.__cache_lock = _thread.allocate_lock()
|
||||
|
||||
def __call__(cls, s, posix_offset=False):
|
||||
key = (s, posix_offset)
|
||||
instance = cls.__instances.get(key, None)
|
||||
|
||||
if instance is None:
|
||||
instance = cls.__instances.setdefault(key,
|
||||
cls.instance(s, posix_offset))
|
||||
|
||||
# This lock may not be necessary in Python 3. See GH issue #901
|
||||
with cls.__cache_lock:
|
||||
cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance)
|
||||
|
||||
# Remove an item if the strong cache is overpopulated
|
||||
if len(cls.__strong_cache) > cls.__strong_cache_size:
|
||||
cls.__strong_cache.popitem(last=False)
|
||||
|
||||
return instance
|
||||
|
||||
@@ -0,0 +1,370 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
This module provides an interface to the native time zone data on Windows,
|
||||
including :py:class:`datetime.tzinfo` implementations.
|
||||
|
||||
Attempting to import this module on a non-Windows platform will raise an
|
||||
:py:obj:`ImportError`.
|
||||
"""
|
||||
# This code was originally contributed by Jeffrey Harris.
|
||||
import datetime
|
||||
import struct
|
||||
|
||||
from six.moves import winreg
|
||||
from six import text_type
|
||||
|
||||
try:
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
except ValueError:
|
||||
# ValueError is raised on non-Windows systems for some horrible reason.
|
||||
raise ImportError("Running tzwin on non-Windows system")
|
||||
|
||||
from ._common import tzrangebase
|
||||
|
||||
__all__ = ["tzwin", "tzwinlocal", "tzres"]
|
||||
|
||||
ONEWEEK = datetime.timedelta(7)
|
||||
|
||||
TZKEYNAMENT = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones"
|
||||
TZKEYNAME9X = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Time Zones"
|
||||
TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"
|
||||
|
||||
|
||||
def _settzkeyname():
|
||||
handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
|
||||
try:
|
||||
winreg.OpenKey(handle, TZKEYNAMENT).Close()
|
||||
TZKEYNAME = TZKEYNAMENT
|
||||
except WindowsError:
|
||||
TZKEYNAME = TZKEYNAME9X
|
||||
handle.Close()
|
||||
return TZKEYNAME
|
||||
|
||||
|
||||
TZKEYNAME = _settzkeyname()
|
||||
|
||||
|
||||
class tzres(object):
|
||||
"""
|
||||
Class for accessing ``tzres.dll``, which contains timezone name related
|
||||
resources.
|
||||
|
||||
.. versionadded:: 2.5.0
|
||||
"""
|
||||
p_wchar = ctypes.POINTER(wintypes.WCHAR) # Pointer to a wide char
|
||||
|
||||
def __init__(self, tzres_loc='tzres.dll'):
|
||||
# Load the user32 DLL so we can load strings from tzres
|
||||
user32 = ctypes.WinDLL('user32')
|
||||
|
||||
# Specify the LoadStringW function
|
||||
user32.LoadStringW.argtypes = (wintypes.HINSTANCE,
|
||||
wintypes.UINT,
|
||||
wintypes.LPWSTR,
|
||||
ctypes.c_int)
|
||||
|
||||
self.LoadStringW = user32.LoadStringW
|
||||
self._tzres = ctypes.WinDLL(tzres_loc)
|
||||
self.tzres_loc = tzres_loc
|
||||
|
||||
def load_name(self, offset):
|
||||
"""
|
||||
Load a timezone name from a DLL offset (integer).
|
||||
|
||||
>>> from dateutil.tzwin import tzres
|
||||
>>> tzr = tzres()
|
||||
>>> print(tzr.load_name(112))
|
||||
'Eastern Standard Time'
|
||||
|
||||
:param offset:
|
||||
A positive integer value referring to a string from the tzres dll.
|
||||
|
||||
.. note::
|
||||
|
||||
Offsets found in the registry are generally of the form
|
||||
``@tzres.dll,-114``. The offset in this case is 114, not -114.
|
||||
|
||||
"""
|
||||
resource = self.p_wchar()
|
||||
lpBuffer = ctypes.cast(ctypes.byref(resource), wintypes.LPWSTR)
|
||||
nchar = self.LoadStringW(self._tzres._handle, offset, lpBuffer, 0)
|
||||
return resource[:nchar]
|
||||
|
||||
def name_from_string(self, tzname_str):
|
||||
"""
|
||||
Parse strings as returned from the Windows registry into the time zone
|
||||
name as defined in the registry.
|
||||
|
||||
>>> from dateutil.tzwin import tzres
|
||||
>>> tzr = tzres()
|
||||
>>> print(tzr.name_from_string('@tzres.dll,-251'))
|
||||
'Dateline Daylight Time'
|
||||
>>> print(tzr.name_from_string('Eastern Standard Time'))
|
||||
'Eastern Standard Time'
|
||||
|
||||
:param tzname_str:
|
||||
A timezone name string as returned from a Windows registry key.
|
||||
|
||||
:return:
|
||||
Returns the localized timezone string from tzres.dll if the string
|
||||
is of the form `@tzres.dll,-offset`, else returns the input string.
|
||||
"""
|
||||
if not tzname_str.startswith('@'):
|
||||
return tzname_str
|
||||
|
||||
name_splt = tzname_str.split(',-')
|
||||
try:
|
||||
offset = int(name_splt[1])
|
||||
except:
|
||||
raise ValueError("Malformed timezone string.")
|
||||
|
||||
return self.load_name(offset)
|
||||
|
||||
|
||||
class tzwinbase(tzrangebase):
|
||||
"""tzinfo class based on win32's timezones available in the registry."""
|
||||
def __init__(self):
|
||||
raise NotImplementedError('tzwinbase is an abstract base class')
|
||||
|
||||
def __eq__(self, other):
|
||||
# Compare on all relevant dimensions, including name.
|
||||
if not isinstance(other, tzwinbase):
|
||||
return NotImplemented
|
||||
|
||||
return (self._std_offset == other._std_offset and
|
||||
self._dst_offset == other._dst_offset and
|
||||
self._stddayofweek == other._stddayofweek and
|
||||
self._dstdayofweek == other._dstdayofweek and
|
||||
self._stdweeknumber == other._stdweeknumber and
|
||||
self._dstweeknumber == other._dstweeknumber and
|
||||
self._stdhour == other._stdhour and
|
||||
self._dsthour == other._dsthour and
|
||||
self._stdminute == other._stdminute and
|
||||
self._dstminute == other._dstminute and
|
||||
self._std_abbr == other._std_abbr and
|
||||
self._dst_abbr == other._dst_abbr)
|
||||
|
||||
@staticmethod
|
||||
def list():
|
||||
"""Return a list of all time zones known to the system."""
|
||||
with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
|
||||
with winreg.OpenKey(handle, TZKEYNAME) as tzkey:
|
||||
result = [winreg.EnumKey(tzkey, i)
|
||||
for i in range(winreg.QueryInfoKey(tzkey)[0])]
|
||||
return result
|
||||
|
||||
def display(self):
|
||||
"""
|
||||
Return the display name of the time zone.
|
||||
"""
|
||||
return self._display
|
||||
|
||||
def transitions(self, year):
|
||||
"""
|
||||
For a given year, get the DST on and off transition times, expressed
|
||||
always on the standard time side. For zones with no transitions, this
|
||||
function returns ``None``.
|
||||
|
||||
:param year:
|
||||
The year whose transitions you would like to query.
|
||||
|
||||
:return:
|
||||
Returns a :class:`tuple` of :class:`datetime.datetime` objects,
|
||||
``(dston, dstoff)`` for zones with an annual DST transition, or
|
||||
``None`` for fixed offset zones.
|
||||
"""
|
||||
|
||||
if not self.hasdst:
|
||||
return None
|
||||
|
||||
dston = picknthweekday(year, self._dstmonth, self._dstdayofweek,
|
||||
self._dsthour, self._dstminute,
|
||||
self._dstweeknumber)
|
||||
|
||||
dstoff = picknthweekday(year, self._stdmonth, self._stddayofweek,
|
||||
self._stdhour, self._stdminute,
|
||||
self._stdweeknumber)
|
||||
|
||||
# Ambiguous dates default to the STD side
|
||||
dstoff -= self._dst_base_offset
|
||||
|
||||
return dston, dstoff
|
||||
|
||||
def _get_hasdst(self):
|
||||
return self._dstmonth != 0
|
||||
|
||||
@property
|
||||
def _dst_base_offset(self):
|
||||
return self._dst_base_offset_
|
||||
|
||||
|
||||
class tzwin(tzwinbase):
|
||||
"""
|
||||
Time zone object created from the zone info in the Windows registry
|
||||
|
||||
These are similar to :py:class:`dateutil.tz.tzrange` objects in that
|
||||
the time zone data is provided in the format of a single offset rule
|
||||
for either 0 or 2 time zone transitions per year.
|
||||
|
||||
:param: name
|
||||
The name of a Windows time zone key, e.g. "Eastern Standard Time".
|
||||
The full list of keys can be retrieved with :func:`tzwin.list`.
|
||||
"""
|
||||
|
||||
def __init__(self, name):
|
||||
self._name = name
|
||||
|
||||
with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
|
||||
tzkeyname = text_type("{kn}\\{name}").format(kn=TZKEYNAME, name=name)
|
||||
with winreg.OpenKey(handle, tzkeyname) as tzkey:
|
||||
keydict = valuestodict(tzkey)
|
||||
|
||||
self._std_abbr = keydict["Std"]
|
||||
self._dst_abbr = keydict["Dlt"]
|
||||
|
||||
self._display = keydict["Display"]
|
||||
|
||||
# See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm
|
||||
tup = struct.unpack("=3l16h", keydict["TZI"])
|
||||
stdoffset = -tup[0]-tup[1] # Bias + StandardBias * -1
|
||||
dstoffset = stdoffset-tup[2] # + DaylightBias * -1
|
||||
self._std_offset = datetime.timedelta(minutes=stdoffset)
|
||||
self._dst_offset = datetime.timedelta(minutes=dstoffset)
|
||||
|
||||
# for the meaning see the win32 TIME_ZONE_INFORMATION structure docs
|
||||
# http://msdn.microsoft.com/en-us/library/windows/desktop/ms725481(v=vs.85).aspx
|
||||
(self._stdmonth,
|
||||
self._stddayofweek, # Sunday = 0
|
||||
self._stdweeknumber, # Last = 5
|
||||
self._stdhour,
|
||||
self._stdminute) = tup[4:9]
|
||||
|
||||
(self._dstmonth,
|
||||
self._dstdayofweek, # Sunday = 0
|
||||
self._dstweeknumber, # Last = 5
|
||||
self._dsthour,
|
||||
self._dstminute) = tup[12:17]
|
||||
|
||||
self._dst_base_offset_ = self._dst_offset - self._std_offset
|
||||
self.hasdst = self._get_hasdst()
|
||||
|
||||
def __repr__(self):
|
||||
return "tzwin(%s)" % repr(self._name)
|
||||
|
||||
def __reduce__(self):
|
||||
return (self.__class__, (self._name,))
|
||||
|
||||
|
||||
class tzwinlocal(tzwinbase):
|
||||
"""
|
||||
Class representing the local time zone information in the Windows registry
|
||||
|
||||
While :class:`dateutil.tz.tzlocal` makes system calls (via the :mod:`time`
|
||||
module) to retrieve time zone information, ``tzwinlocal`` retrieves the
|
||||
rules directly from the Windows registry and creates an object like
|
||||
:class:`dateutil.tz.tzwin`.
|
||||
|
||||
Because Windows does not have an equivalent of :func:`time.tzset`, on
|
||||
Windows, :class:`dateutil.tz.tzlocal` instances will always reflect the
|
||||
time zone settings *at the time that the process was started*, meaning
|
||||
changes to the machine's time zone settings during the run of a program
|
||||
on Windows will **not** be reflected by :class:`dateutil.tz.tzlocal`.
|
||||
Because ``tzwinlocal`` reads the registry directly, it is unaffected by
|
||||
this issue.
|
||||
"""
|
||||
def __init__(self):
|
||||
with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
|
||||
with winreg.OpenKey(handle, TZLOCALKEYNAME) as tzlocalkey:
|
||||
keydict = valuestodict(tzlocalkey)
|
||||
|
||||
self._std_abbr = keydict["StandardName"]
|
||||
self._dst_abbr = keydict["DaylightName"]
|
||||
|
||||
try:
|
||||
tzkeyname = text_type('{kn}\\{sn}').format(kn=TZKEYNAME,
|
||||
sn=self._std_abbr)
|
||||
with winreg.OpenKey(handle, tzkeyname) as tzkey:
|
||||
_keydict = valuestodict(tzkey)
|
||||
self._display = _keydict["Display"]
|
||||
except OSError:
|
||||
self._display = None
|
||||
|
||||
stdoffset = -keydict["Bias"]-keydict["StandardBias"]
|
||||
dstoffset = stdoffset-keydict["DaylightBias"]
|
||||
|
||||
self._std_offset = datetime.timedelta(minutes=stdoffset)
|
||||
self._dst_offset = datetime.timedelta(minutes=dstoffset)
|
||||
|
||||
# For reasons unclear, in this particular key, the day of week has been
|
||||
# moved to the END of the SYSTEMTIME structure.
|
||||
tup = struct.unpack("=8h", keydict["StandardStart"])
|
||||
|
||||
(self._stdmonth,
|
||||
self._stdweeknumber, # Last = 5
|
||||
self._stdhour,
|
||||
self._stdminute) = tup[1:5]
|
||||
|
||||
self._stddayofweek = tup[7]
|
||||
|
||||
tup = struct.unpack("=8h", keydict["DaylightStart"])
|
||||
|
||||
(self._dstmonth,
|
||||
self._dstweeknumber, # Last = 5
|
||||
self._dsthour,
|
||||
self._dstminute) = tup[1:5]
|
||||
|
||||
self._dstdayofweek = tup[7]
|
||||
|
||||
self._dst_base_offset_ = self._dst_offset - self._std_offset
|
||||
self.hasdst = self._get_hasdst()
|
||||
|
||||
def __repr__(self):
|
||||
return "tzwinlocal()"
|
||||
|
||||
def __str__(self):
|
||||
# str will return the standard name, not the daylight name.
|
||||
return "tzwinlocal(%s)" % repr(self._std_abbr)
|
||||
|
||||
def __reduce__(self):
|
||||
return (self.__class__, ())
|
||||
|
||||
|
||||
def picknthweekday(year, month, dayofweek, hour, minute, whichweek):
|
||||
""" dayofweek == 0 means Sunday, whichweek 5 means last instance """
|
||||
first = datetime.datetime(year, month, 1, hour, minute)
|
||||
|
||||
# This will work if dayofweek is ISO weekday (1-7) or Microsoft-style (0-6),
|
||||
# Because 7 % 7 = 0
|
||||
weekdayone = first.replace(day=((dayofweek - first.isoweekday()) % 7) + 1)
|
||||
wd = weekdayone + ((whichweek - 1) * ONEWEEK)
|
||||
if (wd.month != month):
|
||||
wd -= ONEWEEK
|
||||
|
||||
return wd
|
||||
|
||||
|
||||
def valuestodict(key):
|
||||
"""Convert a registry key's values to a dictionary."""
|
||||
dout = {}
|
||||
size = winreg.QueryInfoKey(key)[1]
|
||||
tz_res = None
|
||||
|
||||
for i in range(size):
|
||||
key_name, value, dtype = winreg.EnumValue(key, i)
|
||||
if dtype == winreg.REG_DWORD or dtype == winreg.REG_DWORD_LITTLE_ENDIAN:
|
||||
# If it's a DWORD (32-bit integer), it's stored as unsigned - convert
|
||||
# that to a proper signed integer
|
||||
if value & (1 << 31):
|
||||
value = value - (1 << 32)
|
||||
elif dtype == winreg.REG_SZ:
|
||||
# If it's a reference to the tzres DLL, load the actual string
|
||||
if value.startswith('@tzres'):
|
||||
tz_res = tz_res or tzres()
|
||||
value = tz_res.name_from_string(value)
|
||||
|
||||
value = value.rstrip('\x00') # Remove trailing nulls
|
||||
|
||||
dout[key_name] = value
|
||||
|
||||
return dout
|
||||
@@ -0,0 +1,2 @@
|
||||
# tzwin has moved to dateutil.tz.win
|
||||
from .tz.win import *
|
||||
@@ -0,0 +1,71 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
This module offers general convenience and utility functions for dealing with
|
||||
datetimes.
|
||||
|
||||
.. versionadded:: 2.7.0
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from datetime import datetime, time
|
||||
|
||||
|
||||
def today(tzinfo=None):
|
||||
"""
|
||||
Returns a :py:class:`datetime` representing the current day at midnight
|
||||
|
||||
:param tzinfo:
|
||||
The time zone to attach (also used to determine the current day).
|
||||
|
||||
:return:
|
||||
A :py:class:`datetime.datetime` object representing the current day
|
||||
at midnight.
|
||||
"""
|
||||
|
||||
dt = datetime.now(tzinfo)
|
||||
return datetime.combine(dt.date(), time(0, tzinfo=tzinfo))
|
||||
|
||||
|
||||
def default_tzinfo(dt, tzinfo):
|
||||
"""
|
||||
Sets the ``tzinfo`` parameter on naive datetimes only
|
||||
|
||||
This is useful for example when you are provided a datetime that may have
|
||||
either an implicit or explicit time zone, such as when parsing a time zone
|
||||
string.
|
||||
|
||||
.. doctest::
|
||||
|
||||
>>> from dateutil.tz import tzoffset
|
||||
>>> from dateutil.parser import parse
|
||||
>>> from dateutil.utils import default_tzinfo
|
||||
>>> dflt_tz = tzoffset("EST", -18000)
|
||||
>>> print(default_tzinfo(parse('2014-01-01 12:30 UTC'), dflt_tz))
|
||||
2014-01-01 12:30:00+00:00
|
||||
>>> print(default_tzinfo(parse('2014-01-01 12:30'), dflt_tz))
|
||||
2014-01-01 12:30:00-05:00
|
||||
|
||||
:param dt:
|
||||
The datetime on which to replace the time zone
|
||||
|
||||
:param tzinfo:
|
||||
The :py:class:`datetime.tzinfo` subclass instance to assign to
|
||||
``dt`` if (and only if) it is naive.
|
||||
|
||||
:return:
|
||||
Returns an aware :py:class:`datetime.datetime`.
|
||||
"""
|
||||
if dt.tzinfo is not None:
|
||||
return dt
|
||||
else:
|
||||
return dt.replace(tzinfo=tzinfo)
|
||||
|
||||
|
||||
def within_delta(dt1, dt2, delta):
|
||||
"""
|
||||
Useful for comparing two datetimes that may have a negligible difference
|
||||
to be considered equal.
|
||||
"""
|
||||
delta = abs(delta)
|
||||
difference = dt1 - dt2
|
||||
return -delta <= difference <= delta
|
||||
@@ -0,0 +1,167 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import warnings
|
||||
import json
|
||||
|
||||
from tarfile import TarFile
|
||||
from pkgutil import get_data
|
||||
from io import BytesIO
|
||||
|
||||
from dateutil.tz import tzfile as _tzfile
|
||||
|
||||
__all__ = ["get_zonefile_instance", "gettz", "gettz_db_metadata"]
|
||||
|
||||
ZONEFILENAME = "dateutil-zoneinfo.tar.gz"
|
||||
METADATA_FN = 'METADATA'
|
||||
|
||||
|
||||
class tzfile(_tzfile):
|
||||
def __reduce__(self):
|
||||
return (gettz, (self._filename,))
|
||||
|
||||
|
||||
def getzoneinfofile_stream():
|
||||
try:
|
||||
return BytesIO(get_data(__name__, ZONEFILENAME))
|
||||
except IOError as e: # TODO switch to FileNotFoundError?
|
||||
warnings.warn("I/O error({0}): {1}".format(e.errno, e.strerror))
|
||||
return None
|
||||
|
||||
|
||||
class ZoneInfoFile(object):
|
||||
def __init__(self, zonefile_stream=None):
|
||||
if zonefile_stream is not None:
|
||||
with TarFile.open(fileobj=zonefile_stream) as tf:
|
||||
self.zones = {zf.name: tzfile(tf.extractfile(zf), filename=zf.name)
|
||||
for zf in tf.getmembers()
|
||||
if zf.isfile() and zf.name != METADATA_FN}
|
||||
# deal with links: They'll point to their parent object. Less
|
||||
# waste of memory
|
||||
links = {zl.name: self.zones[zl.linkname]
|
||||
for zl in tf.getmembers() if
|
||||
zl.islnk() or zl.issym()}
|
||||
self.zones.update(links)
|
||||
try:
|
||||
metadata_json = tf.extractfile(tf.getmember(METADATA_FN))
|
||||
metadata_str = metadata_json.read().decode('UTF-8')
|
||||
self.metadata = json.loads(metadata_str)
|
||||
except KeyError:
|
||||
# no metadata in tar file
|
||||
self.metadata = None
|
||||
else:
|
||||
self.zones = {}
|
||||
self.metadata = None
|
||||
|
||||
def get(self, name, default=None):
|
||||
"""
|
||||
Wrapper for :func:`ZoneInfoFile.zones.get`. This is a convenience method
|
||||
for retrieving zones from the zone dictionary.
|
||||
|
||||
:param name:
|
||||
The name of the zone to retrieve. (Generally IANA zone names)
|
||||
|
||||
:param default:
|
||||
The value to return in the event of a missing key.
|
||||
|
||||
.. versionadded:: 2.6.0
|
||||
|
||||
"""
|
||||
return self.zones.get(name, default)
|
||||
|
||||
|
||||
# The current API has gettz as a module function, although in fact it taps into
|
||||
# a stateful class. So as a workaround for now, without changing the API, we
|
||||
# will create a new "global" class instance the first time a user requests a
|
||||
# timezone. Ugly, but adheres to the api.
|
||||
#
|
||||
# TODO: Remove after deprecation period.
|
||||
_CLASS_ZONE_INSTANCE = []
|
||||
|
||||
|
||||
def get_zonefile_instance(new_instance=False):
|
||||
"""
|
||||
This is a convenience function which provides a :class:`ZoneInfoFile`
|
||||
instance using the data provided by the ``dateutil`` package. By default, it
|
||||
caches a single instance of the ZoneInfoFile object and returns that.
|
||||
|
||||
:param new_instance:
|
||||
If ``True``, a new instance of :class:`ZoneInfoFile` is instantiated and
|
||||
used as the cached instance for the next call. Otherwise, new instances
|
||||
are created only as necessary.
|
||||
|
||||
:return:
|
||||
Returns a :class:`ZoneInfoFile` object.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
"""
|
||||
if new_instance:
|
||||
zif = None
|
||||
else:
|
||||
zif = getattr(get_zonefile_instance, '_cached_instance', None)
|
||||
|
||||
if zif is None:
|
||||
zif = ZoneInfoFile(getzoneinfofile_stream())
|
||||
|
||||
get_zonefile_instance._cached_instance = zif
|
||||
|
||||
return zif
|
||||
|
||||
|
||||
def gettz(name):
|
||||
"""
|
||||
This retrieves a time zone from the local zoneinfo tarball that is packaged
|
||||
with dateutil.
|
||||
|
||||
:param name:
|
||||
An IANA-style time zone name, as found in the zoneinfo file.
|
||||
|
||||
:return:
|
||||
Returns a :class:`dateutil.tz.tzfile` time zone object.
|
||||
|
||||
.. warning::
|
||||
It is generally inadvisable to use this function, and it is only
|
||||
provided for API compatibility with earlier versions. This is *not*
|
||||
equivalent to ``dateutil.tz.gettz()``, which selects an appropriate
|
||||
time zone based on the inputs, favoring system zoneinfo. This is ONLY
|
||||
for accessing the dateutil-specific zoneinfo (which may be out of
|
||||
date compared to the system zoneinfo).
|
||||
|
||||
.. deprecated:: 2.6
|
||||
If you need to use a specific zoneinfofile over the system zoneinfo,
|
||||
instantiate a :class:`dateutil.zoneinfo.ZoneInfoFile` object and call
|
||||
:func:`dateutil.zoneinfo.ZoneInfoFile.get(name)` instead.
|
||||
|
||||
Use :func:`get_zonefile_instance` to retrieve an instance of the
|
||||
dateutil-provided zoneinfo.
|
||||
"""
|
||||
warnings.warn("zoneinfo.gettz() will be removed in future versions, "
|
||||
"to use the dateutil-provided zoneinfo files, instantiate a "
|
||||
"ZoneInfoFile object and use ZoneInfoFile.zones.get() "
|
||||
"instead. See the documentation for details.",
|
||||
DeprecationWarning)
|
||||
|
||||
if len(_CLASS_ZONE_INSTANCE) == 0:
|
||||
_CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream()))
|
||||
return _CLASS_ZONE_INSTANCE[0].zones.get(name)
|
||||
|
||||
|
||||
def gettz_db_metadata():
|
||||
""" Get the zonefile metadata
|
||||
|
||||
See `zonefile_metadata`_
|
||||
|
||||
:returns:
|
||||
A dictionary with the database metadata
|
||||
|
||||
.. deprecated:: 2.6
|
||||
See deprecation warning in :func:`zoneinfo.gettz`. To get metadata,
|
||||
query the attribute ``zoneinfo.ZoneInfoFile.metadata``.
|
||||
"""
|
||||
warnings.warn("zoneinfo.gettz_db_metadata() will be removed in future "
|
||||
"versions, to use the dateutil-provided zoneinfo files, "
|
||||
"ZoneInfoFile object and query the 'metadata' attribute "
|
||||
"instead. See the documentation for details.",
|
||||
DeprecationWarning)
|
||||
|
||||
if len(_CLASS_ZONE_INSTANCE) == 0:
|
||||
_CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream()))
|
||||
return _CLASS_ZONE_INSTANCE[0].metadata
|
||||
@@ -0,0 +1,75 @@
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
import json
|
||||
from subprocess import check_call, check_output
|
||||
from tarfile import TarFile
|
||||
|
||||
from dateutil.zoneinfo import METADATA_FN, ZONEFILENAME
|
||||
|
||||
|
||||
def rebuild(filename, tag=None, format="gz", zonegroups=[], metadata=None):
|
||||
"""Rebuild the internal timezone info in dateutil/zoneinfo/zoneinfo*tar*
|
||||
|
||||
filename is the timezone tarball from ``ftp.iana.org/tz``.
|
||||
|
||||
"""
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
zonedir = os.path.join(tmpdir, "zoneinfo")
|
||||
moduledir = os.path.dirname(__file__)
|
||||
try:
|
||||
with TarFile.open(filename) as tf:
|
||||
for name in zonegroups:
|
||||
tf.extract(name, tmpdir)
|
||||
filepaths = [os.path.join(tmpdir, n) for n in zonegroups]
|
||||
|
||||
_run_zic(zonedir, filepaths)
|
||||
|
||||
# write metadata file
|
||||
with open(os.path.join(zonedir, METADATA_FN), 'w') as f:
|
||||
json.dump(metadata, f, indent=4, sort_keys=True)
|
||||
target = os.path.join(moduledir, ZONEFILENAME)
|
||||
with TarFile.open(target, "w:%s" % format) as tf:
|
||||
for entry in os.listdir(zonedir):
|
||||
entrypath = os.path.join(zonedir, entry)
|
||||
tf.add(entrypath, entry)
|
||||
finally:
|
||||
shutil.rmtree(tmpdir)
|
||||
|
||||
|
||||
def _run_zic(zonedir, filepaths):
|
||||
"""Calls the ``zic`` compiler in a compatible way to get a "fat" binary.
|
||||
|
||||
Recent versions of ``zic`` default to ``-b slim``, while older versions
|
||||
don't even have the ``-b`` option (but default to "fat" binaries). The
|
||||
current version of dateutil does not support Version 2+ TZif files, which
|
||||
causes problems when used in conjunction with "slim" binaries, so this
|
||||
function is used to ensure that we always get a "fat" binary.
|
||||
"""
|
||||
|
||||
try:
|
||||
help_text = check_output(["zic", "--help"])
|
||||
except OSError as e:
|
||||
_print_on_nosuchfile(e)
|
||||
raise
|
||||
|
||||
if b"-b " in help_text:
|
||||
bloat_args = ["-b", "fat"]
|
||||
else:
|
||||
bloat_args = []
|
||||
|
||||
check_call(["zic"] + bloat_args + ["-d", zonedir] + filepaths)
|
||||
|
||||
|
||||
def _print_on_nosuchfile(e):
|
||||
"""Print helpful troubleshooting message
|
||||
|
||||
e is an exception raised by subprocess.check_call()
|
||||
|
||||
"""
|
||||
if e.errno == 2:
|
||||
logging.error(
|
||||
"Could not find zic. Perhaps you need to install "
|
||||
"libc-bin or some other package that provides it, "
|
||||
"or it's not in your PATH?")
|
||||
@@ -0,0 +1,396 @@
|
||||
# coding: utf-8
|
||||
|
||||
# flake8: noqa
|
||||
|
||||
"""
|
||||
InfluxDB OSS API Service.
|
||||
|
||||
The InfluxDB v2 API provides a programmatic interface for all interactions with InfluxDB. Access the InfluxDB API using the `/api/v2/` endpoint. # noqa: E501
|
||||
|
||||
OpenAPI spec version: 2.0.0
|
||||
Generated by: https://openapi-generator.tech
|
||||
"""
|
||||
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
# import apis into sdk package
|
||||
from influxdb_client.service.authorizations_service import AuthorizationsService
|
||||
from influxdb_client.service.backup_service import BackupService
|
||||
from influxdb_client.service.bucket_schemas_service import BucketSchemasService
|
||||
from influxdb_client.service.buckets_service import BucketsService
|
||||
from influxdb_client.service.cells_service import CellsService
|
||||
from influxdb_client.service.checks_service import ChecksService
|
||||
from influxdb_client.service.config_service import ConfigService
|
||||
from influxdb_client.service.dbr_ps_service import DBRPsService
|
||||
from influxdb_client.service.dashboards_service import DashboardsService
|
||||
from influxdb_client.service.delete_service import DeleteService
|
||||
from influxdb_client.service.health_service import HealthService
|
||||
from influxdb_client.service.invokable_scripts_service import InvokableScriptsService
|
||||
from influxdb_client.service.labels_service import LabelsService
|
||||
from influxdb_client.service.legacy_authorizations_service import LegacyAuthorizationsService
|
||||
from influxdb_client.service.metrics_service import MetricsService
|
||||
from influxdb_client.service.notification_endpoints_service import NotificationEndpointsService
|
||||
from influxdb_client.service.notification_rules_service import NotificationRulesService
|
||||
from influxdb_client.service.organizations_service import OrganizationsService
|
||||
from influxdb_client.service.ping_service import PingService
|
||||
from influxdb_client.service.query_service import QueryService
|
||||
from influxdb_client.service.ready_service import ReadyService
|
||||
from influxdb_client.service.remote_connections_service import RemoteConnectionsService
|
||||
from influxdb_client.service.replications_service import ReplicationsService
|
||||
from influxdb_client.service.resources_service import ResourcesService
|
||||
from influxdb_client.service.restore_service import RestoreService
|
||||
from influxdb_client.service.routes_service import RoutesService
|
||||
from influxdb_client.service.rules_service import RulesService
|
||||
from influxdb_client.service.scraper_targets_service import ScraperTargetsService
|
||||
from influxdb_client.service.secrets_service import SecretsService
|
||||
from influxdb_client.service.setup_service import SetupService
|
||||
from influxdb_client.service.signin_service import SigninService
|
||||
from influxdb_client.service.signout_service import SignoutService
|
||||
from influxdb_client.service.sources_service import SourcesService
|
||||
from influxdb_client.service.tasks_service import TasksService
|
||||
from influxdb_client.service.telegraf_plugins_service import TelegrafPluginsService
|
||||
from influxdb_client.service.telegrafs_service import TelegrafsService
|
||||
from influxdb_client.service.templates_service import TemplatesService
|
||||
from influxdb_client.service.users_service import UsersService
|
||||
from influxdb_client.service.variables_service import VariablesService
|
||||
from influxdb_client.service.views_service import ViewsService
|
||||
from influxdb_client.service.write_service import WriteService
|
||||
|
||||
from influxdb_client.configuration import Configuration
|
||||
# import models into sdk package
|
||||
from influxdb_client.domain.ast_response import ASTResponse
|
||||
from influxdb_client.domain.add_resource_member_request_body import AddResourceMemberRequestBody
|
||||
from influxdb_client.domain.analyze_query_response import AnalyzeQueryResponse
|
||||
from influxdb_client.domain.analyze_query_response_errors import AnalyzeQueryResponseErrors
|
||||
from influxdb_client.domain.array_expression import ArrayExpression
|
||||
from influxdb_client.domain.authorization import Authorization
|
||||
from influxdb_client.domain.authorization_post_request import AuthorizationPostRequest
|
||||
from influxdb_client.domain.authorization_update_request import AuthorizationUpdateRequest
|
||||
from influxdb_client.domain.authorizations import Authorizations
|
||||
from influxdb_client.domain.axes import Axes
|
||||
from influxdb_client.domain.axis import Axis
|
||||
from influxdb_client.domain.axis_scale import AxisScale
|
||||
from influxdb_client.domain.bad_statement import BadStatement
|
||||
from influxdb_client.domain.band_view_properties import BandViewProperties
|
||||
from influxdb_client.domain.binary_expression import BinaryExpression
|
||||
from influxdb_client.domain.block import Block
|
||||
from influxdb_client.domain.boolean_literal import BooleanLiteral
|
||||
from influxdb_client.domain.bucket import Bucket
|
||||
from influxdb_client.domain.bucket_links import BucketLinks
|
||||
from influxdb_client.domain.bucket_metadata_manifest import BucketMetadataManifest
|
||||
from influxdb_client.domain.bucket_retention_rules import BucketRetentionRules
|
||||
from influxdb_client.domain.bucket_shard_mapping import BucketShardMapping
|
||||
from influxdb_client.domain.buckets import Buckets
|
||||
from influxdb_client.domain.builder_aggregate_function_type import BuilderAggregateFunctionType
|
||||
from influxdb_client.domain.builder_config import BuilderConfig
|
||||
from influxdb_client.domain.builder_config_aggregate_window import BuilderConfigAggregateWindow
|
||||
from influxdb_client.domain.builder_functions_type import BuilderFunctionsType
|
||||
from influxdb_client.domain.builder_tags_type import BuilderTagsType
|
||||
from influxdb_client.domain.builtin_statement import BuiltinStatement
|
||||
from influxdb_client.domain.call_expression import CallExpression
|
||||
from influxdb_client.domain.cell import Cell
|
||||
from influxdb_client.domain.cell_links import CellLinks
|
||||
from influxdb_client.domain.cell_update import CellUpdate
|
||||
from influxdb_client.domain.cell_with_view_properties import CellWithViewProperties
|
||||
from influxdb_client.domain.check import Check
|
||||
from influxdb_client.domain.check_base import CheckBase
|
||||
from influxdb_client.domain.check_base_links import CheckBaseLinks
|
||||
from influxdb_client.domain.check_discriminator import CheckDiscriminator
|
||||
from influxdb_client.domain.check_patch import CheckPatch
|
||||
from influxdb_client.domain.check_status_level import CheckStatusLevel
|
||||
from influxdb_client.domain.check_view_properties import CheckViewProperties
|
||||
from influxdb_client.domain.checks import Checks
|
||||
from influxdb_client.domain.column_data_type import ColumnDataType
|
||||
from influxdb_client.domain.column_semantic_type import ColumnSemanticType
|
||||
from influxdb_client.domain.conditional_expression import ConditionalExpression
|
||||
from influxdb_client.domain.config import Config
|
||||
from influxdb_client.domain.constant_variable_properties import ConstantVariableProperties
|
||||
from influxdb_client.domain.create_cell import CreateCell
|
||||
from influxdb_client.domain.create_dashboard_request import CreateDashboardRequest
|
||||
from influxdb_client.domain.custom_check import CustomCheck
|
||||
from influxdb_client.domain.dbrp import DBRP
|
||||
from influxdb_client.domain.dbrp_create import DBRPCreate
|
||||
from influxdb_client.domain.dbrp_get import DBRPGet
|
||||
from influxdb_client.domain.dbrp_update import DBRPUpdate
|
||||
from influxdb_client.domain.dbr_ps import DBRPs
|
||||
from influxdb_client.domain.dashboard import Dashboard
|
||||
from influxdb_client.domain.dashboard_color import DashboardColor
|
||||
from influxdb_client.domain.dashboard_query import DashboardQuery
|
||||
from influxdb_client.domain.dashboard_with_view_properties import DashboardWithViewProperties
|
||||
from influxdb_client.domain.dashboards import Dashboards
|
||||
from influxdb_client.domain.date_time_literal import DateTimeLiteral
|
||||
from influxdb_client.domain.deadman_check import DeadmanCheck
|
||||
from influxdb_client.domain.decimal_places import DecimalPlaces
|
||||
from influxdb_client.domain.delete_predicate_request import DeletePredicateRequest
|
||||
from influxdb_client.domain.dialect import Dialect
|
||||
from influxdb_client.domain.dict_expression import DictExpression
|
||||
from influxdb_client.domain.dict_item import DictItem
|
||||
from influxdb_client.domain.duration import Duration
|
||||
from influxdb_client.domain.duration_literal import DurationLiteral
|
||||
from influxdb_client.domain.error import Error
|
||||
from influxdb_client.domain.expression import Expression
|
||||
from influxdb_client.domain.expression_statement import ExpressionStatement
|
||||
from influxdb_client.domain.field import Field
|
||||
from influxdb_client.domain.file import File
|
||||
from influxdb_client.domain.float_literal import FloatLiteral
|
||||
from influxdb_client.domain.flux_response import FluxResponse
|
||||
from influxdb_client.domain.flux_suggestion import FluxSuggestion
|
||||
from influxdb_client.domain.flux_suggestions import FluxSuggestions
|
||||
from influxdb_client.domain.function_expression import FunctionExpression
|
||||
from influxdb_client.domain.gauge_view_properties import GaugeViewProperties
|
||||
from influxdb_client.domain.greater_threshold import GreaterThreshold
|
||||
from influxdb_client.domain.http_notification_endpoint import HTTPNotificationEndpoint
|
||||
from influxdb_client.domain.http_notification_rule import HTTPNotificationRule
|
||||
from influxdb_client.domain.http_notification_rule_base import HTTPNotificationRuleBase
|
||||
from influxdb_client.domain.health_check import HealthCheck
|
||||
from influxdb_client.domain.heatmap_view_properties import HeatmapViewProperties
|
||||
from influxdb_client.domain.histogram_view_properties import HistogramViewProperties
|
||||
from influxdb_client.domain.identifier import Identifier
|
||||
from influxdb_client.domain.import_declaration import ImportDeclaration
|
||||
from influxdb_client.domain.index_expression import IndexExpression
|
||||
from influxdb_client.domain.integer_literal import IntegerLiteral
|
||||
from influxdb_client.domain.is_onboarding import IsOnboarding
|
||||
from influxdb_client.domain.label import Label
|
||||
from influxdb_client.domain.label_create_request import LabelCreateRequest
|
||||
from influxdb_client.domain.label_mapping import LabelMapping
|
||||
from influxdb_client.domain.label_response import LabelResponse
|
||||
from influxdb_client.domain.label_update import LabelUpdate
|
||||
from influxdb_client.domain.labels_response import LabelsResponse
|
||||
from influxdb_client.domain.language_request import LanguageRequest
|
||||
from influxdb_client.domain.legacy_authorization_post_request import LegacyAuthorizationPostRequest
|
||||
from influxdb_client.domain.lesser_threshold import LesserThreshold
|
||||
from influxdb_client.domain.line_plus_single_stat_properties import LinePlusSingleStatProperties
|
||||
from influxdb_client.domain.line_protocol_error import LineProtocolError
|
||||
from influxdb_client.domain.line_protocol_length_error import LineProtocolLengthError
|
||||
from influxdb_client.domain.links import Links
|
||||
from influxdb_client.domain.list_stacks_response import ListStacksResponse
|
||||
from influxdb_client.domain.log_event import LogEvent
|
||||
from influxdb_client.domain.logical_expression import LogicalExpression
|
||||
from influxdb_client.domain.logs import Logs
|
||||
from influxdb_client.domain.map_variable_properties import MapVariableProperties
|
||||
from influxdb_client.domain.markdown_view_properties import MarkdownViewProperties
|
||||
from influxdb_client.domain.measurement_schema import MeasurementSchema
|
||||
from influxdb_client.domain.measurement_schema_column import MeasurementSchemaColumn
|
||||
from influxdb_client.domain.measurement_schema_create_request import MeasurementSchemaCreateRequest
|
||||
from influxdb_client.domain.measurement_schema_list import MeasurementSchemaList
|
||||
from influxdb_client.domain.measurement_schema_update_request import MeasurementSchemaUpdateRequest
|
||||
from influxdb_client.domain.member_assignment import MemberAssignment
|
||||
from influxdb_client.domain.member_expression import MemberExpression
|
||||
from influxdb_client.domain.metadata_backup import MetadataBackup
|
||||
from influxdb_client.domain.model_property import ModelProperty
|
||||
from influxdb_client.domain.mosaic_view_properties import MosaicViewProperties
|
||||
from influxdb_client.domain.node import Node
|
||||
from influxdb_client.domain.notification_endpoint import NotificationEndpoint
|
||||
from influxdb_client.domain.notification_endpoint_base import NotificationEndpointBase
|
||||
from influxdb_client.domain.notification_endpoint_base_links import NotificationEndpointBaseLinks
|
||||
from influxdb_client.domain.notification_endpoint_discriminator import NotificationEndpointDiscriminator
|
||||
from influxdb_client.domain.notification_endpoint_type import NotificationEndpointType
|
||||
from influxdb_client.domain.notification_endpoint_update import NotificationEndpointUpdate
|
||||
from influxdb_client.domain.notification_endpoints import NotificationEndpoints
|
||||
from influxdb_client.domain.notification_rule import NotificationRule
|
||||
from influxdb_client.domain.notification_rule_base import NotificationRuleBase
|
||||
from influxdb_client.domain.notification_rule_base_links import NotificationRuleBaseLinks
|
||||
from influxdb_client.domain.notification_rule_discriminator import NotificationRuleDiscriminator
|
||||
from influxdb_client.domain.notification_rule_update import NotificationRuleUpdate
|
||||
from influxdb_client.domain.notification_rules import NotificationRules
|
||||
from influxdb_client.domain.object_expression import ObjectExpression
|
||||
from influxdb_client.domain.onboarding_request import OnboardingRequest
|
||||
from influxdb_client.domain.onboarding_response import OnboardingResponse
|
||||
from influxdb_client.domain.option_statement import OptionStatement
|
||||
from influxdb_client.domain.organization import Organization
|
||||
from influxdb_client.domain.organization_links import OrganizationLinks
|
||||
from influxdb_client.domain.organizations import Organizations
|
||||
from influxdb_client.domain.package import Package
|
||||
from influxdb_client.domain.package_clause import PackageClause
|
||||
from influxdb_client.domain.pager_duty_notification_endpoint import PagerDutyNotificationEndpoint
|
||||
from influxdb_client.domain.pager_duty_notification_rule import PagerDutyNotificationRule
|
||||
from influxdb_client.domain.pager_duty_notification_rule_base import PagerDutyNotificationRuleBase
|
||||
from influxdb_client.domain.paren_expression import ParenExpression
|
||||
from influxdb_client.domain.password_reset_body import PasswordResetBody
|
||||
from influxdb_client.domain.patch_bucket_request import PatchBucketRequest
|
||||
from influxdb_client.domain.patch_dashboard_request import PatchDashboardRequest
|
||||
from influxdb_client.domain.patch_organization_request import PatchOrganizationRequest
|
||||
from influxdb_client.domain.patch_retention_rule import PatchRetentionRule
|
||||
from influxdb_client.domain.patch_stack_request import PatchStackRequest
|
||||
from influxdb_client.domain.patch_stack_request_additional_resources import PatchStackRequestAdditionalResources
|
||||
from influxdb_client.domain.permission import Permission
|
||||
from influxdb_client.domain.permission_resource import PermissionResource
|
||||
from influxdb_client.domain.pipe_expression import PipeExpression
|
||||
from influxdb_client.domain.pipe_literal import PipeLiteral
|
||||
from influxdb_client.domain.post_bucket_request import PostBucketRequest
|
||||
from influxdb_client.domain.post_check import PostCheck
|
||||
from influxdb_client.domain.post_notification_endpoint import PostNotificationEndpoint
|
||||
from influxdb_client.domain.post_notification_rule import PostNotificationRule
|
||||
from influxdb_client.domain.post_organization_request import PostOrganizationRequest
|
||||
from influxdb_client.domain.post_restore_kv_response import PostRestoreKVResponse
|
||||
from influxdb_client.domain.post_stack_request import PostStackRequest
|
||||
from influxdb_client.domain.property_key import PropertyKey
|
||||
from influxdb_client.domain.query import Query
|
||||
from influxdb_client.domain.query_edit_mode import QueryEditMode
|
||||
from influxdb_client.domain.query_variable_properties import QueryVariableProperties
|
||||
from influxdb_client.domain.query_variable_properties_values import QueryVariablePropertiesValues
|
||||
from influxdb_client.domain.range_threshold import RangeThreshold
|
||||
from influxdb_client.domain.ready import Ready
|
||||
from influxdb_client.domain.regexp_literal import RegexpLiteral
|
||||
from influxdb_client.domain.remote_connection import RemoteConnection
|
||||
from influxdb_client.domain.remote_connection_creation_request import RemoteConnectionCreationRequest
|
||||
from influxdb_client.domain.remote_connection_update_request import RemoteConnectionUpdateRequest
|
||||
from influxdb_client.domain.remote_connections import RemoteConnections
|
||||
from influxdb_client.domain.renamable_field import RenamableField
|
||||
from influxdb_client.domain.replication import Replication
|
||||
from influxdb_client.domain.replication_creation_request import ReplicationCreationRequest
|
||||
from influxdb_client.domain.replication_update_request import ReplicationUpdateRequest
|
||||
from influxdb_client.domain.replications import Replications
|
||||
from influxdb_client.domain.resource_member import ResourceMember
|
||||
from influxdb_client.domain.resource_members import ResourceMembers
|
||||
from influxdb_client.domain.resource_members_links import ResourceMembersLinks
|
||||
from influxdb_client.domain.resource_owner import ResourceOwner
|
||||
from influxdb_client.domain.resource_owners import ResourceOwners
|
||||
from influxdb_client.domain.restored_bucket_mappings import RestoredBucketMappings
|
||||
from influxdb_client.domain.retention_policy_manifest import RetentionPolicyManifest
|
||||
from influxdb_client.domain.return_statement import ReturnStatement
|
||||
from influxdb_client.domain.routes import Routes
|
||||
from influxdb_client.domain.routes_external import RoutesExternal
|
||||
from influxdb_client.domain.routes_query import RoutesQuery
|
||||
from influxdb_client.domain.routes_system import RoutesSystem
|
||||
from influxdb_client.domain.rule_status_level import RuleStatusLevel
|
||||
from influxdb_client.domain.run import Run
|
||||
from influxdb_client.domain.run_links import RunLinks
|
||||
from influxdb_client.domain.run_manually import RunManually
|
||||
from influxdb_client.domain.runs import Runs
|
||||
from influxdb_client.domain.smtp_notification_rule import SMTPNotificationRule
|
||||
from influxdb_client.domain.smtp_notification_rule_base import SMTPNotificationRuleBase
|
||||
from influxdb_client.domain.scatter_view_properties import ScatterViewProperties
|
||||
from influxdb_client.domain.schema_type import SchemaType
|
||||
from influxdb_client.domain.scraper_target_request import ScraperTargetRequest
|
||||
from influxdb_client.domain.scraper_target_response import ScraperTargetResponse
|
||||
from influxdb_client.domain.scraper_target_responses import ScraperTargetResponses
|
||||
from influxdb_client.domain.script import Script
|
||||
from influxdb_client.domain.script_create_request import ScriptCreateRequest
|
||||
from influxdb_client.domain.script_invocation_params import ScriptInvocationParams
|
||||
from influxdb_client.domain.script_language import ScriptLanguage
|
||||
from influxdb_client.domain.script_update_request import ScriptUpdateRequest
|
||||
from influxdb_client.domain.scripts import Scripts
|
||||
from influxdb_client.domain.secret_keys import SecretKeys
|
||||
from influxdb_client.domain.secret_keys_response import SecretKeysResponse
|
||||
from influxdb_client.domain.shard_group_manifest import ShardGroupManifest
|
||||
from influxdb_client.domain.shard_manifest import ShardManifest
|
||||
from influxdb_client.domain.shard_owner import ShardOwner
|
||||
from influxdb_client.domain.simple_table_view_properties import SimpleTableViewProperties
|
||||
from influxdb_client.domain.single_stat_view_properties import SingleStatViewProperties
|
||||
from influxdb_client.domain.slack_notification_endpoint import SlackNotificationEndpoint
|
||||
from influxdb_client.domain.slack_notification_rule import SlackNotificationRule
|
||||
from influxdb_client.domain.slack_notification_rule_base import SlackNotificationRuleBase
|
||||
from influxdb_client.domain.source import Source
|
||||
from influxdb_client.domain.source_links import SourceLinks
|
||||
from influxdb_client.domain.sources import Sources
|
||||
from influxdb_client.domain.stack import Stack
|
||||
from influxdb_client.domain.stack_associations import StackAssociations
|
||||
from influxdb_client.domain.stack_events import StackEvents
|
||||
from influxdb_client.domain.stack_links import StackLinks
|
||||
from influxdb_client.domain.stack_resources import StackResources
|
||||
from influxdb_client.domain.statement import Statement
|
||||
from influxdb_client.domain.static_legend import StaticLegend
|
||||
from influxdb_client.domain.status_rule import StatusRule
|
||||
from influxdb_client.domain.string_literal import StringLiteral
|
||||
from influxdb_client.domain.subscription_manifest import SubscriptionManifest
|
||||
from influxdb_client.domain.table_view_properties import TableViewProperties
|
||||
from influxdb_client.domain.table_view_properties_table_options import TableViewPropertiesTableOptions
|
||||
from influxdb_client.domain.tag_rule import TagRule
|
||||
from influxdb_client.domain.task import Task
|
||||
from influxdb_client.domain.task_create_request import TaskCreateRequest
|
||||
from influxdb_client.domain.task_links import TaskLinks
|
||||
from influxdb_client.domain.task_status_type import TaskStatusType
|
||||
from influxdb_client.domain.task_update_request import TaskUpdateRequest
|
||||
from influxdb_client.domain.tasks import Tasks
|
||||
from influxdb_client.domain.telegraf import Telegraf
|
||||
from influxdb_client.domain.telegraf_plugin import TelegrafPlugin
|
||||
from influxdb_client.domain.telegraf_plugin_request import TelegrafPluginRequest
|
||||
from influxdb_client.domain.telegraf_plugin_request_plugins import TelegrafPluginRequestPlugins
|
||||
from influxdb_client.domain.telegraf_plugins import TelegrafPlugins
|
||||
from influxdb_client.domain.telegraf_request import TelegrafRequest
|
||||
from influxdb_client.domain.telegraf_request_metadata import TelegrafRequestMetadata
|
||||
from influxdb_client.domain.telegrafs import Telegrafs
|
||||
from influxdb_client.domain.telegram_notification_endpoint import TelegramNotificationEndpoint
|
||||
from influxdb_client.domain.telegram_notification_rule import TelegramNotificationRule
|
||||
from influxdb_client.domain.telegram_notification_rule_base import TelegramNotificationRuleBase
|
||||
from influxdb_client.domain.template_apply import TemplateApply
|
||||
from influxdb_client.domain.template_apply_remotes import TemplateApplyRemotes
|
||||
from influxdb_client.domain.template_apply_template import TemplateApplyTemplate
|
||||
from influxdb_client.domain.template_chart import TemplateChart
|
||||
from influxdb_client.domain.template_export_by_id import TemplateExportByID
|
||||
from influxdb_client.domain.template_export_by_id_org_ids import TemplateExportByIDOrgIDs
|
||||
from influxdb_client.domain.template_export_by_id_resource_filters import TemplateExportByIDResourceFilters
|
||||
from influxdb_client.domain.template_export_by_id_resources import TemplateExportByIDResources
|
||||
from influxdb_client.domain.template_kind import TemplateKind
|
||||
from influxdb_client.domain.template_summary import TemplateSummary
|
||||
from influxdb_client.domain.template_summary_diff import TemplateSummaryDiff
|
||||
from influxdb_client.domain.template_summary_diff_buckets import TemplateSummaryDiffBuckets
|
||||
from influxdb_client.domain.template_summary_diff_buckets_new_old import TemplateSummaryDiffBucketsNewOld
|
||||
from influxdb_client.domain.template_summary_diff_checks import TemplateSummaryDiffChecks
|
||||
from influxdb_client.domain.template_summary_diff_dashboards import TemplateSummaryDiffDashboards
|
||||
from influxdb_client.domain.template_summary_diff_dashboards_new_old import TemplateSummaryDiffDashboardsNewOld
|
||||
from influxdb_client.domain.template_summary_diff_label_mappings import TemplateSummaryDiffLabelMappings
|
||||
from influxdb_client.domain.template_summary_diff_labels import TemplateSummaryDiffLabels
|
||||
from influxdb_client.domain.template_summary_diff_labels_new_old import TemplateSummaryDiffLabelsNewOld
|
||||
from influxdb_client.domain.template_summary_diff_notification_endpoints import TemplateSummaryDiffNotificationEndpoints
|
||||
from influxdb_client.domain.template_summary_diff_notification_rules import TemplateSummaryDiffNotificationRules
|
||||
from influxdb_client.domain.template_summary_diff_notification_rules_new_old import TemplateSummaryDiffNotificationRulesNewOld
|
||||
from influxdb_client.domain.template_summary_diff_tasks import TemplateSummaryDiffTasks
|
||||
from influxdb_client.domain.template_summary_diff_tasks_new_old import TemplateSummaryDiffTasksNewOld
|
||||
from influxdb_client.domain.template_summary_diff_telegraf_configs import TemplateSummaryDiffTelegrafConfigs
|
||||
from influxdb_client.domain.template_summary_diff_variables import TemplateSummaryDiffVariables
|
||||
from influxdb_client.domain.template_summary_diff_variables_new_old import TemplateSummaryDiffVariablesNewOld
|
||||
from influxdb_client.domain.template_summary_errors import TemplateSummaryErrors
|
||||
from influxdb_client.domain.template_summary_label import TemplateSummaryLabel
|
||||
from influxdb_client.domain.template_summary_label_properties import TemplateSummaryLabelProperties
|
||||
from influxdb_client.domain.template_summary_summary import TemplateSummarySummary
|
||||
from influxdb_client.domain.template_summary_summary_buckets import TemplateSummarySummaryBuckets
|
||||
from influxdb_client.domain.template_summary_summary_dashboards import TemplateSummarySummaryDashboards
|
||||
from influxdb_client.domain.template_summary_summary_label_mappings import TemplateSummarySummaryLabelMappings
|
||||
from influxdb_client.domain.template_summary_summary_notification_rules import TemplateSummarySummaryNotificationRules
|
||||
from influxdb_client.domain.template_summary_summary_status_rules import TemplateSummarySummaryStatusRules
|
||||
from influxdb_client.domain.template_summary_summary_tag_rules import TemplateSummarySummaryTagRules
|
||||
from influxdb_client.domain.template_summary_summary_tasks import TemplateSummarySummaryTasks
|
||||
from influxdb_client.domain.template_summary_summary_variables import TemplateSummarySummaryVariables
|
||||
from influxdb_client.domain.test_statement import TestStatement
|
||||
from influxdb_client.domain.threshold import Threshold
|
||||
from influxdb_client.domain.threshold_base import ThresholdBase
|
||||
from influxdb_client.domain.threshold_check import ThresholdCheck
|
||||
from influxdb_client.domain.unary_expression import UnaryExpression
|
||||
from influxdb_client.domain.unsigned_integer_literal import UnsignedIntegerLiteral
|
||||
from influxdb_client.domain.user import User
|
||||
from influxdb_client.domain.user_response import UserResponse
|
||||
from influxdb_client.domain.user_response_links import UserResponseLinks
|
||||
from influxdb_client.domain.users import Users
|
||||
from influxdb_client.domain.variable import Variable
|
||||
from influxdb_client.domain.variable_assignment import VariableAssignment
|
||||
from influxdb_client.domain.variable_links import VariableLinks
|
||||
from influxdb_client.domain.variable_properties import VariableProperties
|
||||
from influxdb_client.domain.variables import Variables
|
||||
from influxdb_client.domain.view import View
|
||||
from influxdb_client.domain.view_links import ViewLinks
|
||||
from influxdb_client.domain.view_properties import ViewProperties
|
||||
from influxdb_client.domain.views import Views
|
||||
from influxdb_client.domain.write_precision import WritePrecision
|
||||
from influxdb_client.domain.xy_geom import XYGeom
|
||||
from influxdb_client.domain.xy_view_properties import XYViewProperties
|
||||
|
||||
from influxdb_client.client.authorizations_api import AuthorizationsApi
|
||||
from influxdb_client.client.bucket_api import BucketsApi
|
||||
from influxdb_client.client.delete_api import DeleteApi
|
||||
from influxdb_client.client.invokable_scripts_api import InvokableScriptsApi
|
||||
from influxdb_client.client.labels_api import LabelsApi
|
||||
from influxdb_client.client.organizations_api import OrganizationsApi
|
||||
from influxdb_client.client.query_api import QueryApi
|
||||
from influxdb_client.client.tasks_api import TasksApi
|
||||
from influxdb_client.client.users_api import UsersApi
|
||||
from influxdb_client.client.write_api import WriteApi, WriteOptions
|
||||
from influxdb_client.client.influxdb_client import InfluxDBClient
|
||||
from influxdb_client.client.logging_handler import InfluxLoggingHandler
|
||||
from influxdb_client.client.write.point import Point
|
||||
|
||||
from influxdb_client.version import VERSION
|
||||
|
||||
__version__ = VERSION
|
||||
@@ -0,0 +1 @@
|
||||
"""Asynchronous REST APIs."""
|
||||
@@ -0,0 +1,663 @@
|
||||
# coding: utf-8
|
||||
"""
|
||||
InfluxDB OSS API Service.
|
||||
|
||||
The InfluxDB v2 API provides a programmatic interface for all interactions with InfluxDB. Access the InfluxDB API using the `/api/v2/` endpoint. # noqa: E501
|
||||
|
||||
OpenAPI spec version: 2.0.0
|
||||
Generated by: https://openapi-generator.tech
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
from multiprocessing.pool import ThreadPool
|
||||
from urllib.parse import quote
|
||||
|
||||
import influxdb_client.domain
|
||||
from influxdb_client import SigninService
|
||||
from influxdb_client import SignoutService
|
||||
from influxdb_client._async import rest
|
||||
from influxdb_client.configuration import Configuration
|
||||
from influxdb_client.rest import _requires_create_user_session, _requires_expire_user_session
|
||||
|
||||
|
||||
class ApiClientAsync(object):
|
||||
"""Generic API client for OpenAPI client library Build.
|
||||
|
||||
OpenAPI generic API client. This client handles the client-
|
||||
server communication, and is invariant across implementations. Specifics of
|
||||
the methods and models for each application are generated from the OpenAPI
|
||||
templates.
|
||||
|
||||
NOTE: This class is auto generated by OpenAPI Generator.
|
||||
Ref: https://openapi-generator.tech
|
||||
Do not edit the class manually.
|
||||
|
||||
:param configuration: .Configuration object for this client
|
||||
:param header_name: a header to pass when making calls to the API.
|
||||
:param header_value: a header value to pass when making calls to
|
||||
the API.
|
||||
:param cookie: a cookie to include in the header when making calls
|
||||
to the API
|
||||
:param pool_threads: The number of threads to use for async requests
|
||||
to the API. More threads means more concurrent API requests.
|
||||
"""
|
||||
|
||||
PRIMITIVE_TYPES = (float, bool, bytes, str, int)
|
||||
NATIVE_TYPES_MAPPING = {
|
||||
'int': int,
|
||||
'long': int,
|
||||
'float': float,
|
||||
'str': str,
|
||||
'bool': bool,
|
||||
'date': datetime.date,
|
||||
'datetime': datetime.datetime,
|
||||
'object': object,
|
||||
}
|
||||
_pool = None
|
||||
|
||||
def __init__(self, configuration=None, header_name=None, header_value=None,
|
||||
cookie=None, pool_threads=None, **kwargs):
|
||||
"""Initialize generic API client."""
|
||||
if configuration is None:
|
||||
configuration = Configuration()
|
||||
self.configuration = configuration
|
||||
self.pool_threads = pool_threads
|
||||
|
||||
self.rest_client = rest.RESTClientObjectAsync(configuration, **kwargs)
|
||||
self.default_headers = {}
|
||||
if header_name is not None:
|
||||
self.default_headers[header_name] = header_value
|
||||
self.cookie = cookie
|
||||
# Set default User-Agent.
|
||||
from influxdb_client import VERSION
|
||||
self.user_agent = f'influxdb-client-python/{VERSION}'
|
||||
|
||||
async def close(self):
|
||||
"""Dispose api client."""
|
||||
await self._signout()
|
||||
await self.rest_client.close()
|
||||
"""Dispose pools."""
|
||||
if self._pool:
|
||||
self._pool.close()
|
||||
self._pool.join()
|
||||
self._pool = None
|
||||
|
||||
@property
|
||||
def pool(self):
|
||||
"""Create thread pool on first request avoids instantiating unused threadpool for blocking clients."""
|
||||
if self._pool is None:
|
||||
self._pool = ThreadPool(self.pool_threads)
|
||||
return self._pool
|
||||
|
||||
@property
|
||||
def user_agent(self):
|
||||
"""User agent for this API client."""
|
||||
return self.default_headers['User-Agent']
|
||||
|
||||
@user_agent.setter
|
||||
def user_agent(self, value):
|
||||
"""Set User agent for this API client."""
|
||||
self.default_headers['User-Agent'] = value
|
||||
|
||||
def set_default_header(self, header_name, header_value):
|
||||
"""Set HTTP header for this API client."""
|
||||
self.default_headers[header_name] = header_value
|
||||
|
||||
async def __call_api(
|
||||
self, resource_path, method, path_params=None,
|
||||
query_params=None, header_params=None, body=None, post_params=None,
|
||||
files=None, response_type=None, auth_settings=None,
|
||||
_return_http_data_only=None, collection_formats=None,
|
||||
_preload_content=True, _request_timeout=None, urlopen_kw=None):
|
||||
|
||||
config = self.configuration
|
||||
await self._signin(resource_path=resource_path)
|
||||
|
||||
# header parameters
|
||||
header_params = header_params or {}
|
||||
config.update_request_header_params(resource_path, header_params)
|
||||
header_params.update(self.default_headers)
|
||||
if self.cookie:
|
||||
header_params['Cookie'] = self.cookie
|
||||
if header_params:
|
||||
header_params = self.sanitize_for_serialization(header_params)
|
||||
header_params = dict(self.parameters_to_tuples(header_params,
|
||||
collection_formats))
|
||||
|
||||
# path parameters
|
||||
if path_params:
|
||||
path_params = self.sanitize_for_serialization(path_params)
|
||||
path_params = self.parameters_to_tuples(path_params,
|
||||
collection_formats)
|
||||
for k, v in path_params:
|
||||
# specified safe chars, encode everything
|
||||
resource_path = resource_path.replace(
|
||||
'{%s}' % k,
|
||||
quote(str(v), safe=config.safe_chars_for_path_param)
|
||||
)
|
||||
|
||||
# query parameters
|
||||
if query_params:
|
||||
query_params = self.sanitize_for_serialization(query_params)
|
||||
query_params = self.parameters_to_tuples(query_params,
|
||||
collection_formats)
|
||||
|
||||
# post parameters
|
||||
if post_params or files:
|
||||
post_params = self.prepare_post_parameters(post_params, files)
|
||||
post_params = self.sanitize_for_serialization(post_params)
|
||||
post_params = self.parameters_to_tuples(post_params,
|
||||
collection_formats)
|
||||
|
||||
# auth setting
|
||||
self.update_params_for_auth(header_params, query_params, auth_settings)
|
||||
|
||||
# body
|
||||
if body:
|
||||
body = self.sanitize_for_serialization(body)
|
||||
body = config.update_request_body(resource_path, body)
|
||||
|
||||
# request url
|
||||
url = self.configuration.host + resource_path
|
||||
|
||||
urlopen_kw = urlopen_kw or {}
|
||||
|
||||
# perform request and return response
|
||||
response_data = await self.request(
|
||||
method, url, query_params=query_params, headers=header_params,
|
||||
post_params=post_params, body=body,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout, **urlopen_kw)
|
||||
|
||||
self.last_response = response_data
|
||||
|
||||
return_data = response_data
|
||||
if _preload_content:
|
||||
# deserialize response data
|
||||
if response_type:
|
||||
return_data = self.deserialize(response_data, response_type)
|
||||
else:
|
||||
return_data = None
|
||||
|
||||
if _return_http_data_only is not False:
|
||||
return return_data
|
||||
else:
|
||||
return (return_data, response_data.status,
|
||||
response_data.getheaders())
|
||||
|
||||
def sanitize_for_serialization(self, obj):
|
||||
"""Build a JSON POST object.
|
||||
|
||||
If obj is None, return None.
|
||||
If obj is str, int, long, float, bool, return directly.
|
||||
If obj is datetime.datetime, datetime.date
|
||||
convert to string in iso8601 format.
|
||||
If obj is list, sanitize each element in the list.
|
||||
If obj is dict, return the dict.
|
||||
If obj is OpenAPI model, return the properties dict.
|
||||
|
||||
:param obj: The data to serialize.
|
||||
:return: The serialized form of data.
|
||||
"""
|
||||
if obj is None:
|
||||
return None
|
||||
elif isinstance(obj, self.PRIMITIVE_TYPES):
|
||||
return obj
|
||||
elif isinstance(obj, list):
|
||||
return [self.sanitize_for_serialization(sub_obj)
|
||||
for sub_obj in obj]
|
||||
elif isinstance(obj, tuple):
|
||||
return tuple(self.sanitize_for_serialization(sub_obj)
|
||||
for sub_obj in obj)
|
||||
elif isinstance(obj, (datetime.datetime, datetime.date)):
|
||||
return obj.isoformat()
|
||||
|
||||
if isinstance(obj, dict):
|
||||
obj_dict = obj
|
||||
else:
|
||||
# Convert model obj to dict except
|
||||
# attributes `openapi_types`, `attribute_map`
|
||||
# and attributes which value is not None.
|
||||
# Convert attribute name to json key in
|
||||
# model definition for request.
|
||||
obj_dict = {obj.attribute_map[attr]: getattr(obj, attr)
|
||||
for attr, _ in obj.openapi_types.items()
|
||||
if getattr(obj, attr) is not None}
|
||||
|
||||
return {key: self.sanitize_for_serialization(val)
|
||||
for key, val in obj_dict.items()}
|
||||
|
||||
def deserialize(self, response, response_type):
|
||||
"""Deserializes response into an object.
|
||||
|
||||
:param response: RESTResponse object to be deserialized.
|
||||
:param response_type: class literal for
|
||||
deserialized object, or string of class name.
|
||||
|
||||
:return: deserialized object.
|
||||
"""
|
||||
# handle file downloading
|
||||
# save response body into a tmp file and return the instance
|
||||
if response_type == "file":
|
||||
return self.__deserialize_file(response)
|
||||
|
||||
# fetch data from response object
|
||||
try:
|
||||
data = json.loads(response.data)
|
||||
except ValueError:
|
||||
data = response.data
|
||||
|
||||
return self.__deserialize(data, response_type)
|
||||
|
||||
def __deserialize(self, data, klass):
|
||||
"""Deserializes dict, list, str into an object.
|
||||
|
||||
:param data: dict, list or str.
|
||||
:param klass: class literal, or string of class name.
|
||||
|
||||
:return: object.
|
||||
"""
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
if type(klass) == str:
|
||||
if klass.startswith('list['):
|
||||
sub_kls = re.match(r'list\[(.*)\]', klass).group(1)
|
||||
return [self.__deserialize(sub_data, sub_kls)
|
||||
for sub_data in data]
|
||||
|
||||
if klass.startswith('dict('):
|
||||
sub_kls = re.match(r'dict\(([^,]*), (.*)\)', klass).group(2)
|
||||
return {k: self.__deserialize(v, sub_kls)
|
||||
for k, v in data.items()}
|
||||
|
||||
# convert str to class
|
||||
if klass in self.NATIVE_TYPES_MAPPING:
|
||||
klass = self.NATIVE_TYPES_MAPPING[klass]
|
||||
else:
|
||||
klass = getattr(influxdb_client.domain, klass)
|
||||
|
||||
if klass in self.PRIMITIVE_TYPES:
|
||||
return self.__deserialize_primitive(data, klass)
|
||||
elif klass == object:
|
||||
return self.__deserialize_object(data)
|
||||
elif klass == datetime.date:
|
||||
return self.__deserialize_date(data)
|
||||
elif klass == datetime.datetime:
|
||||
return self.__deserialize_datatime(data)
|
||||
else:
|
||||
return self.__deserialize_model(data, klass)
|
||||
|
||||
def call_api(self, resource_path, method,
|
||||
path_params=None, query_params=None, header_params=None,
|
||||
body=None, post_params=None, files=None,
|
||||
response_type=None, auth_settings=None, async_req=None,
|
||||
_return_http_data_only=None, collection_formats=None,
|
||||
_preload_content=True, _request_timeout=None, urlopen_kw=None):
|
||||
"""Make the HTTP request (synchronous) and Return deserialized data.
|
||||
|
||||
To make an async_req request, set the async_req parameter.
|
||||
|
||||
:param resource_path: Path to method endpoint.
|
||||
:param method: Method to call.
|
||||
:param path_params: Path parameters in the url.
|
||||
:param query_params: Query parameters in the url.
|
||||
:param header_params: Header parameters to be
|
||||
placed in the request header.
|
||||
:param body: Request body.
|
||||
:param post_params dict: Request post form parameters,
|
||||
for `application/x-www-form-urlencoded`, `multipart/form-data`.
|
||||
:param auth_settings list: Auth Settings names for the request.
|
||||
:param response: Response data type.
|
||||
:param files dict: key -> filename, value -> filepath,
|
||||
for `multipart/form-data`.
|
||||
:param async_req bool: execute request asynchronously
|
||||
:param _return_http_data_only: response data without head status code
|
||||
and headers
|
||||
:param collection_formats: dict of collection formats for path, query,
|
||||
header, and post parameters.
|
||||
:param _preload_content: if False, the urllib3.HTTPResponse object will
|
||||
be returned without reading/decoding response
|
||||
data. Default is True.
|
||||
:param _request_timeout: timeout setting for this request. If one
|
||||
number provided, it will be total request
|
||||
timeout. It can also be a pair (tuple) of
|
||||
(connection, read) timeouts.
|
||||
:param urlopen_kw: Additional parameters are passed to
|
||||
:meth:`urllib3.request.RequestMethods.request`
|
||||
:return:
|
||||
If async_req parameter is True,
|
||||
the request will be called asynchronously.
|
||||
The method will return the request thread.
|
||||
If parameter async_req is False or missing,
|
||||
then the method will return the response directly.
|
||||
"""
|
||||
if not async_req:
|
||||
return self.__call_api(resource_path, method,
|
||||
path_params, query_params, header_params,
|
||||
body, post_params, files,
|
||||
response_type, auth_settings,
|
||||
_return_http_data_only, collection_formats,
|
||||
_preload_content, _request_timeout, urlopen_kw)
|
||||
else:
|
||||
thread = self.pool.apply_async(self.__call_api, (resource_path,
|
||||
method, path_params, query_params,
|
||||
header_params, body,
|
||||
post_params, files,
|
||||
response_type, auth_settings,
|
||||
_return_http_data_only,
|
||||
collection_formats,
|
||||
_preload_content, _request_timeout, urlopen_kw))
|
||||
return thread
|
||||
|
||||
def request(self, method, url, query_params=None, headers=None,
|
||||
post_params=None, body=None, _preload_content=True,
|
||||
_request_timeout=None, **urlopen_kw):
|
||||
"""Make the HTTP request using RESTClient."""
|
||||
if method == "GET":
|
||||
return self.rest_client.GET(url,
|
||||
query_params=query_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
headers=headers,
|
||||
**urlopen_kw)
|
||||
elif method == "HEAD":
|
||||
return self.rest_client.HEAD(url,
|
||||
query_params=query_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
headers=headers,
|
||||
**urlopen_kw)
|
||||
elif method == "OPTIONS":
|
||||
return self.rest_client.OPTIONS(url,
|
||||
query_params=query_params,
|
||||
headers=headers,
|
||||
post_params=post_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body,
|
||||
**urlopen_kw)
|
||||
elif method == "POST":
|
||||
return self.rest_client.POST(url,
|
||||
query_params=query_params,
|
||||
headers=headers,
|
||||
post_params=post_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body,
|
||||
**urlopen_kw)
|
||||
elif method == "PUT":
|
||||
return self.rest_client.PUT(url,
|
||||
query_params=query_params,
|
||||
headers=headers,
|
||||
post_params=post_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body,
|
||||
**urlopen_kw)
|
||||
elif method == "PATCH":
|
||||
return self.rest_client.PATCH(url,
|
||||
query_params=query_params,
|
||||
headers=headers,
|
||||
post_params=post_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body,
|
||||
**urlopen_kw)
|
||||
elif method == "DELETE":
|
||||
return self.rest_client.DELETE(url,
|
||||
query_params=query_params,
|
||||
headers=headers,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body,
|
||||
**urlopen_kw)
|
||||
else:
|
||||
raise ValueError(
|
||||
"http method must be `GET`, `HEAD`, `OPTIONS`,"
|
||||
" `POST`, `PATCH`, `PUT` or `DELETE`."
|
||||
)
|
||||
|
||||
def parameters_to_tuples(self, params, collection_formats):
|
||||
"""Get parameters as list of tuples, formatting collections.
|
||||
|
||||
:param params: Parameters as dict or list of two-tuples
|
||||
:param dict collection_formats: Parameter collection formats
|
||||
:return: Parameters as list of tuples, collections formatted
|
||||
"""
|
||||
new_params = []
|
||||
if collection_formats is None:
|
||||
collection_formats = {}
|
||||
for k, v in params.items() if isinstance(params, dict) else params: # noqa: E501
|
||||
if k in collection_formats:
|
||||
collection_format = collection_formats[k]
|
||||
if collection_format == 'multi':
|
||||
new_params.extend((k, value) for value in v)
|
||||
else:
|
||||
if collection_format == 'ssv':
|
||||
delimiter = ' '
|
||||
elif collection_format == 'tsv':
|
||||
delimiter = '\t'
|
||||
elif collection_format == 'pipes':
|
||||
delimiter = '|'
|
||||
else: # csv is the default
|
||||
delimiter = ','
|
||||
new_params.append(
|
||||
(k, delimiter.join(str(value) for value in v)))
|
||||
else:
|
||||
new_params.append((k, v))
|
||||
return new_params
|
||||
|
||||
def prepare_post_parameters(self, post_params=None, files=None):
|
||||
"""Build form parameters.
|
||||
|
||||
:param post_params: Normal form parameters.
|
||||
:param files: File parameters.
|
||||
:return: Form parameters with files.
|
||||
"""
|
||||
params = []
|
||||
|
||||
if post_params:
|
||||
params = post_params
|
||||
|
||||
if files:
|
||||
for k, v in files.items():
|
||||
if not v:
|
||||
continue
|
||||
file_names = v if type(v) is list else [v]
|
||||
for n in file_names:
|
||||
with open(n, 'rb') as f:
|
||||
filename = os.path.basename(f.name)
|
||||
filedata = f.read()
|
||||
mimetype = (mimetypes.guess_type(filename)[0] or
|
||||
'application/octet-stream')
|
||||
params.append(
|
||||
tuple([k, tuple([filename, filedata, mimetype])]))
|
||||
|
||||
return params
|
||||
|
||||
def select_header_accept(self, accepts):
|
||||
"""Return `Accept` based on an array of accepts provided.
|
||||
|
||||
:param accepts: List of headers.
|
||||
:return: Accept (e.g. application/json).
|
||||
"""
|
||||
if not accepts:
|
||||
return
|
||||
|
||||
accepts = [x.lower() for x in accepts]
|
||||
|
||||
if 'application/json' in accepts:
|
||||
return 'application/json'
|
||||
else:
|
||||
return ', '.join(accepts)
|
||||
|
||||
def select_header_content_type(self, content_types):
|
||||
"""Return `Content-Type` based on an array of content_types provided.
|
||||
|
||||
:param content_types: List of content-types.
|
||||
:return: Content-Type (e.g. application/json).
|
||||
"""
|
||||
if not content_types:
|
||||
return 'application/json'
|
||||
|
||||
content_types = [x.lower() for x in content_types]
|
||||
|
||||
if 'application/json' in content_types or '*/*' in content_types:
|
||||
return 'application/json'
|
||||
else:
|
||||
return content_types[0]
|
||||
|
||||
def update_params_for_auth(self, headers, querys, auth_settings):
|
||||
"""Update header and query params based on authentication setting.
|
||||
|
||||
:param headers: Header parameters dict to be updated.
|
||||
:param querys: Query parameters tuple list to be updated.
|
||||
:param auth_settings: Authentication setting identifiers list.
|
||||
"""
|
||||
if not auth_settings:
|
||||
return
|
||||
|
||||
for auth in auth_settings:
|
||||
auth_setting = self.configuration.auth_settings().get(auth)
|
||||
if auth_setting:
|
||||
if not auth_setting['value']:
|
||||
continue
|
||||
elif auth_setting['in'] == 'header':
|
||||
headers[auth_setting['key']] = auth_setting['value']
|
||||
elif auth_setting['in'] == 'query':
|
||||
querys.append((auth_setting['key'], auth_setting['value']))
|
||||
else:
|
||||
raise ValueError(
|
||||
'Authentication token must be in `query` or `header`'
|
||||
)
|
||||
|
||||
def __deserialize_file(self, response):
|
||||
"""Deserializes body to file.
|
||||
|
||||
Saves response body into a file in a temporary folder,
|
||||
using the filename from the `Content-Disposition` header if provided.
|
||||
|
||||
:param response: RESTResponse.
|
||||
:return: file path.
|
||||
"""
|
||||
fd, path = tempfile.mkstemp(dir=self.configuration.temp_folder_path)
|
||||
os.close(fd)
|
||||
os.remove(path)
|
||||
|
||||
content_disposition = response.getheader("Content-Disposition")
|
||||
if content_disposition:
|
||||
filename = re.search(r'filename=[\'"]?([^\'"\s]+)[\'"]?',
|
||||
content_disposition).group(1)
|
||||
path = os.path.join(os.path.dirname(path), filename)
|
||||
|
||||
with open(path, "wb") as f:
|
||||
f.write(response.data)
|
||||
|
||||
return path
|
||||
|
||||
def __deserialize_primitive(self, data, klass):
|
||||
"""Deserializes string to primitive type.
|
||||
|
||||
:param data: str.
|
||||
:param klass: class literal.
|
||||
|
||||
:return: int, long, float, str, bool.
|
||||
"""
|
||||
try:
|
||||
return klass(data)
|
||||
except UnicodeEncodeError:
|
||||
return str(data)
|
||||
except TypeError:
|
||||
return data
|
||||
|
||||
def __deserialize_object(self, value):
|
||||
"""Return an original value.
|
||||
|
||||
:return: object.
|
||||
"""
|
||||
return value
|
||||
|
||||
def __deserialize_date(self, string):
|
||||
"""Deserializes string to date.
|
||||
|
||||
:param string: str.
|
||||
:return: date.
|
||||
"""
|
||||
try:
|
||||
from dateutil.parser import parse
|
||||
return parse(string).date()
|
||||
except ImportError:
|
||||
return string
|
||||
except ValueError:
|
||||
raise rest.ApiException(
|
||||
status=0,
|
||||
reason="Failed to parse `{0}` as date object".format(string)
|
||||
)
|
||||
|
||||
def __deserialize_datatime(self, string):
|
||||
"""Deserializes string to datetime.
|
||||
|
||||
The string should be in iso8601 datetime format.
|
||||
|
||||
:param string: str.
|
||||
:return: datetime.
|
||||
"""
|
||||
try:
|
||||
from dateutil.parser import parse
|
||||
return parse(string)
|
||||
except ImportError:
|
||||
return string
|
||||
except ValueError:
|
||||
raise rest.ApiException(
|
||||
status=0,
|
||||
reason=(
|
||||
"Failed to parse `{0}` as datetime object"
|
||||
.format(string)
|
||||
)
|
||||
)
|
||||
|
||||
def __deserialize_model(self, data, klass):
|
||||
"""Deserializes list or dict to model.
|
||||
|
||||
:param data: dict, list.
|
||||
:param klass: class literal.
|
||||
:return: model object.
|
||||
"""
|
||||
if not klass.openapi_types and not hasattr(klass,
|
||||
'get_real_child_model'):
|
||||
return data
|
||||
|
||||
kwargs = {}
|
||||
if klass.openapi_types is not None:
|
||||
for attr, attr_type in klass.openapi_types.items():
|
||||
if (data is not None and
|
||||
klass.attribute_map[attr] in data and
|
||||
isinstance(data, (list, dict))):
|
||||
value = data[klass.attribute_map[attr]]
|
||||
kwargs[attr] = self.__deserialize(value, attr_type)
|
||||
|
||||
instance = klass(**kwargs)
|
||||
|
||||
if hasattr(instance, 'get_real_child_model'):
|
||||
klass_name = instance.get_real_child_model(data)
|
||||
if klass_name:
|
||||
instance = self.__deserialize(data, klass_name)
|
||||
return instance
|
||||
|
||||
async def _signin(self, resource_path: str):
|
||||
if _requires_create_user_session(self.configuration, self.cookie, resource_path):
|
||||
http_info = await SigninService(self).post_signin_async(_return_http_data_only=False)
|
||||
self.cookie = http_info[2]['set-cookie']
|
||||
|
||||
async def _signout(self):
|
||||
if _requires_expire_user_session(self.configuration, self.cookie):
|
||||
await SignoutService(self).post_signout_async()
|
||||
self.cookie = None
|
||||
@@ -0,0 +1,309 @@
|
||||
"""
|
||||
InfluxDB OSS API Service.
|
||||
|
||||
The InfluxDB v2 API provides a programmatic interface for all interactions with InfluxDB. Access the InfluxDB API using the `/api/v2/` endpoint. # noqa: E501
|
||||
|
||||
OpenAPI spec version: 2.0.0
|
||||
Generated by: https://openapi-generator.tech
|
||||
"""
|
||||
|
||||
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
import ssl
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import aiohttp
|
||||
|
||||
from influxdb_client.rest import ApiException
|
||||
from influxdb_client.rest import _BaseRESTClient
|
||||
from influxdb_client.rest import _UTF_8_encoding
|
||||
|
||||
|
||||
async def _on_request_start(session, trace_config_ctx, params):
|
||||
_BaseRESTClient.log_request(params.method, params.url)
|
||||
_BaseRESTClient.log_headers(params.headers, '>>>')
|
||||
|
||||
|
||||
async def _on_request_chunk_sent(session, context, params):
|
||||
if params.chunk:
|
||||
_BaseRESTClient.log_body(params.chunk, '>>>')
|
||||
|
||||
|
||||
async def _on_request_end(session, trace_config_ctx, params):
|
||||
_BaseRESTClient.log_response(params.response.status)
|
||||
_BaseRESTClient.log_headers(params.headers, '<<<')
|
||||
|
||||
response_content = params.response.content
|
||||
data = bytearray()
|
||||
while True:
|
||||
chunk = await response_content.read(100)
|
||||
if not chunk:
|
||||
break
|
||||
data += chunk
|
||||
if data:
|
||||
_BaseRESTClient.log_body(data.decode(_UTF_8_encoding), '<<<')
|
||||
response_content.unread_data(data=data)
|
||||
|
||||
|
||||
class RESTResponseAsync(io.IOBase):
|
||||
"""NOTE: This class is auto generated by OpenAPI Generator.
|
||||
|
||||
Ref: https://openapi-generator.tech
|
||||
Do not edit the class manually.
|
||||
"""
|
||||
|
||||
def __init__(self, resp, data):
|
||||
"""Initialize with HTTP response."""
|
||||
self.aiohttp_response = resp
|
||||
self.status = resp.status
|
||||
self.reason = resp.reason
|
||||
self.data = data
|
||||
|
||||
def getheaders(self):
|
||||
"""Return a CIMultiDictProxy of the response headers."""
|
||||
return self.aiohttp_response.headers
|
||||
|
||||
def getheader(self, name, default=None):
|
||||
"""Return a given response header."""
|
||||
return self.aiohttp_response.headers.get(name, default)
|
||||
|
||||
|
||||
class RESTClientObjectAsync(object):
|
||||
"""NOTE: This class is auto generated by OpenAPI Generator.
|
||||
|
||||
Ref: https://openapi-generator.tech
|
||||
Do not edit the class manually.
|
||||
"""
|
||||
|
||||
def __init__(self, configuration, pools_size=4, maxsize=None, **kwargs):
|
||||
"""Initialize REST client."""
|
||||
# maxsize is number of requests to host that are allowed in parallel
|
||||
if maxsize is None:
|
||||
maxsize = configuration.connection_pool_maxsize
|
||||
|
||||
if configuration.ssl_context is None:
|
||||
ssl_context = ssl.create_default_context(cafile=configuration.ssl_ca_cert)
|
||||
if configuration.cert_file:
|
||||
ssl_context.load_cert_chain(
|
||||
certfile=configuration.cert_file, keyfile=configuration.cert_key_file,
|
||||
password=configuration.cert_key_password
|
||||
)
|
||||
|
||||
if not configuration.verify_ssl:
|
||||
ssl_context.check_hostname = False
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
else:
|
||||
ssl_context = configuration.ssl_context
|
||||
|
||||
connector = aiohttp.TCPConnector(
|
||||
limit=maxsize,
|
||||
ssl=ssl_context
|
||||
)
|
||||
|
||||
self.proxy = configuration.proxy
|
||||
self.proxy_headers = configuration.proxy_headers
|
||||
self.allow_redirects = kwargs.get('allow_redirects', True)
|
||||
self.max_redirects = kwargs.get('max_redirects', 10)
|
||||
|
||||
# configure tracing
|
||||
trace_config = aiohttp.TraceConfig()
|
||||
trace_config.on_request_start.append(_on_request_start)
|
||||
trace_config.on_request_chunk_sent.append(_on_request_chunk_sent)
|
||||
trace_config.on_request_end.append(_on_request_end)
|
||||
|
||||
# timeout
|
||||
if isinstance(configuration.timeout, (int, float,)): # noqa: E501,F821
|
||||
timeout = aiohttp.ClientTimeout(total=configuration.timeout / 1_000)
|
||||
elif isinstance(configuration.timeout, aiohttp.ClientTimeout):
|
||||
timeout = configuration.timeout
|
||||
else:
|
||||
timeout = aiohttp.client.DEFAULT_TIMEOUT
|
||||
|
||||
# https pool manager
|
||||
_client_session_type = kwargs.get('client_session_type', aiohttp.ClientSession)
|
||||
_client_session_kwargs = kwargs.get('client_session_kwargs', {})
|
||||
self.pool_manager = _client_session_type(
|
||||
connector=connector,
|
||||
timeout=timeout,
|
||||
trace_configs=[trace_config] if configuration.debug else None,
|
||||
**_client_session_kwargs
|
||||
)
|
||||
|
||||
async def close(self):
|
||||
"""Dispose connection pool manager."""
|
||||
await self.pool_manager.close()
|
||||
|
||||
async def request(self, method, url, query_params=None, headers=None,
|
||||
body=None, post_params=None, _preload_content=True,
|
||||
_request_timeout=None):
|
||||
"""Execute request.
|
||||
|
||||
:param method: http request method
|
||||
:param url: http request url
|
||||
:param query_params: query parameters in the url
|
||||
:param headers: http request headers
|
||||
:param body: request json body, for `application/json`
|
||||
:param post_params: request post parameters,
|
||||
`application/x-www-form-urlencoded`
|
||||
and `multipart/form-data`
|
||||
:param _preload_content: this is a non-applicable field for
|
||||
the AiohttpClient.
|
||||
:param _request_timeout: timeout setting for this request. If one
|
||||
number provided, it will be total request
|
||||
timeout. It can also be a pair (tuple) of
|
||||
(connection, read) timeouts.
|
||||
"""
|
||||
method = method.upper()
|
||||
assert method in ['GET', 'HEAD', 'DELETE', 'POST', 'PUT',
|
||||
'PATCH', 'OPTIONS']
|
||||
|
||||
if post_params and body:
|
||||
raise ValueError(
|
||||
"body parameter cannot be used with post_params parameter."
|
||||
)
|
||||
|
||||
post_params = post_params or {}
|
||||
headers = headers or {}
|
||||
|
||||
if 'Content-Type' not in headers:
|
||||
headers['Content-Type'] = 'application/json'
|
||||
|
||||
args = {
|
||||
"method": method,
|
||||
"url": url,
|
||||
"headers": headers,
|
||||
"allow_redirects": self.allow_redirects,
|
||||
"max_redirects": self.max_redirects
|
||||
}
|
||||
|
||||
if self.proxy:
|
||||
args["proxy"] = self.proxy
|
||||
if self.proxy_headers:
|
||||
args["proxy_headers"] = self.proxy_headers
|
||||
|
||||
if query_params:
|
||||
args["url"] += '?' + urlencode(query_params)
|
||||
|
||||
# For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE`
|
||||
if method in ['POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE']:
|
||||
if re.search('json', headers['Content-Type'], re.IGNORECASE):
|
||||
if body is not None:
|
||||
body = json.dumps(body)
|
||||
args["data"] = body
|
||||
elif headers['Content-Type'] == 'application/x-www-form-urlencoded': # noqa: E501
|
||||
args["data"] = aiohttp.FormData(post_params)
|
||||
elif headers['Content-Type'] == 'multipart/form-data':
|
||||
# must del headers['Content-Type'], or the correct
|
||||
# Content-Type which generated by aiohttp
|
||||
del headers['Content-Type']
|
||||
data = aiohttp.FormData()
|
||||
for param in post_params:
|
||||
k, v = param
|
||||
if isinstance(v, tuple) and len(v) == 3:
|
||||
data.add_field(k,
|
||||
value=v[1],
|
||||
filename=v[0],
|
||||
content_type=v[2])
|
||||
else:
|
||||
data.add_field(k, v)
|
||||
args["data"] = data
|
||||
|
||||
# Pass a `bytes` parameter directly in the body to support
|
||||
# other content types than Json when `body` argument is provided
|
||||
# in serialized form
|
||||
elif isinstance(body, bytes):
|
||||
args["data"] = body
|
||||
else:
|
||||
# Cannot generate the request from given parameters
|
||||
msg = """Cannot prepare a request message for provided
|
||||
arguments. Please check that your arguments match
|
||||
declared content type."""
|
||||
raise ApiException(status=0, reason=msg)
|
||||
|
||||
r = await self.pool_manager.request(**args)
|
||||
if _preload_content:
|
||||
|
||||
data = await r.read()
|
||||
r = RESTResponseAsync(r, data)
|
||||
|
||||
if not 200 <= r.status <= 299:
|
||||
raise ApiException(http_resp=r)
|
||||
|
||||
return r
|
||||
|
||||
async def GET(self, url, headers=None, query_params=None,
|
||||
_preload_content=True, _request_timeout=None):
|
||||
"""Perform GET HTTP request."""
|
||||
return (await self.request("GET", url,
|
||||
headers=headers,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
query_params=query_params))
|
||||
|
||||
async def HEAD(self, url, headers=None, query_params=None,
|
||||
_preload_content=True, _request_timeout=None):
|
||||
"""Perform HEAD HTTP request."""
|
||||
return (await self.request("HEAD", url,
|
||||
headers=headers,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
query_params=query_params))
|
||||
|
||||
async def OPTIONS(self, url, headers=None, query_params=None,
|
||||
post_params=None, body=None, _preload_content=True,
|
||||
_request_timeout=None):
|
||||
"""Perform OPTIONS HTTP request."""
|
||||
return (await self.request("OPTIONS", url,
|
||||
headers=headers,
|
||||
query_params=query_params,
|
||||
post_params=post_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body))
|
||||
|
||||
async def DELETE(self, url, headers=None, query_params=None, body=None,
|
||||
_preload_content=True, _request_timeout=None):
|
||||
"""Perform DELETE HTTP request."""
|
||||
return (await self.request("DELETE", url,
|
||||
headers=headers,
|
||||
query_params=query_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body))
|
||||
|
||||
async def POST(self, url, headers=None, query_params=None,
|
||||
post_params=None, body=None, _preload_content=True,
|
||||
_request_timeout=None):
|
||||
"""Perform POST HTTP request."""
|
||||
return (await self.request("POST", url,
|
||||
headers=headers,
|
||||
query_params=query_params,
|
||||
post_params=post_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body))
|
||||
|
||||
async def PUT(self, url, headers=None, query_params=None, post_params=None,
|
||||
body=None, _preload_content=True, _request_timeout=None):
|
||||
"""Perform PUT HTTP request."""
|
||||
return (await self.request("PUT", url,
|
||||
headers=headers,
|
||||
query_params=query_params,
|
||||
post_params=post_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body))
|
||||
|
||||
async def PATCH(self, url, headers=None, query_params=None,
|
||||
post_params=None, body=None, _preload_content=True,
|
||||
_request_timeout=None):
|
||||
"""Perform PATCH HTTP request."""
|
||||
return (await self.request("PATCH", url,
|
||||
headers=headers,
|
||||
query_params=query_params,
|
||||
post_params=post_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body))
|
||||
@@ -0,0 +1 @@
|
||||
"""Synchronous REST APIs."""
|
||||
@@ -0,0 +1,663 @@
|
||||
# coding: utf-8
|
||||
"""
|
||||
InfluxDB OSS API Service.
|
||||
|
||||
The InfluxDB v2 API provides a programmatic interface for all interactions with InfluxDB. Access the InfluxDB API using the `/api/v2/` endpoint. # noqa: E501
|
||||
|
||||
OpenAPI spec version: 2.0.0
|
||||
Generated by: https://openapi-generator.tech
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
from multiprocessing.pool import ThreadPool
|
||||
from urllib.parse import quote
|
||||
|
||||
import influxdb_client.domain
|
||||
from influxdb_client import SigninService
|
||||
from influxdb_client import SignoutService
|
||||
from influxdb_client._sync import rest
|
||||
from influxdb_client.configuration import Configuration
|
||||
from influxdb_client.rest import _requires_create_user_session, _requires_expire_user_session
|
||||
|
||||
|
||||
class ApiClient(object):
|
||||
"""Generic API client for OpenAPI client library Build.
|
||||
|
||||
OpenAPI generic API client. This client handles the client-
|
||||
server communication, and is invariant across implementations. Specifics of
|
||||
the methods and models for each application are generated from the OpenAPI
|
||||
templates.
|
||||
|
||||
NOTE: This class is auto generated by OpenAPI Generator.
|
||||
Ref: https://openapi-generator.tech
|
||||
Do not edit the class manually.
|
||||
|
||||
:param configuration: .Configuration object for this client
|
||||
:param header_name: a header to pass when making calls to the API.
|
||||
:param header_value: a header value to pass when making calls to
|
||||
the API.
|
||||
:param cookie: a cookie to include in the header when making calls
|
||||
to the API
|
||||
:param pool_threads: The number of threads to use for async requests
|
||||
to the API. More threads means more concurrent API requests.
|
||||
"""
|
||||
|
||||
PRIMITIVE_TYPES = (float, bool, bytes, str, int)
|
||||
NATIVE_TYPES_MAPPING = {
|
||||
'int': int,
|
||||
'long': int,
|
||||
'float': float,
|
||||
'str': str,
|
||||
'bool': bool,
|
||||
'date': datetime.date,
|
||||
'datetime': datetime.datetime,
|
||||
'object': object,
|
||||
}
|
||||
_pool = None
|
||||
|
||||
def __init__(self, configuration=None, header_name=None, header_value=None,
|
||||
cookie=None, pool_threads=None, retries=False):
|
||||
"""Initialize generic API client."""
|
||||
if configuration is None:
|
||||
configuration = Configuration()
|
||||
self.configuration = configuration
|
||||
self.pool_threads = pool_threads
|
||||
|
||||
self.rest_client = rest.RESTClientObject(configuration, retries=retries)
|
||||
self.default_headers = {}
|
||||
if header_name is not None:
|
||||
self.default_headers[header_name] = header_value
|
||||
self.cookie = cookie
|
||||
# Set default User-Agent.
|
||||
from influxdb_client import VERSION
|
||||
self.user_agent = f'influxdb-client-python/{VERSION}'
|
||||
|
||||
def __del__(self):
|
||||
"""Dispose pools."""
|
||||
self._signout()
|
||||
if self._pool:
|
||||
self._pool.close()
|
||||
self._pool.join()
|
||||
self._pool = None
|
||||
if self.rest_client and self.rest_client.pool_manager and hasattr(self.rest_client.pool_manager, 'clear'):
|
||||
self.rest_client.pool_manager.clear()
|
||||
|
||||
@property
|
||||
def pool(self):
|
||||
"""Create thread pool on first request avoids instantiating unused threadpool for blocking clients."""
|
||||
if self._pool is None:
|
||||
self._pool = ThreadPool(self.pool_threads)
|
||||
return self._pool
|
||||
|
||||
@property
|
||||
def user_agent(self):
|
||||
"""User agent for this API client."""
|
||||
return self.default_headers['User-Agent']
|
||||
|
||||
@user_agent.setter
|
||||
def user_agent(self, value):
|
||||
"""Set User agent for this API client."""
|
||||
self.default_headers['User-Agent'] = value
|
||||
|
||||
def set_default_header(self, header_name, header_value):
|
||||
"""Set HTTP header for this API client."""
|
||||
self.default_headers[header_name] = header_value
|
||||
|
||||
def __call_api(
|
||||
self, resource_path, method, path_params=None,
|
||||
query_params=None, header_params=None, body=None, post_params=None,
|
||||
files=None, response_type=None, auth_settings=None,
|
||||
_return_http_data_only=None, collection_formats=None,
|
||||
_preload_content=True, _request_timeout=None, urlopen_kw=None):
|
||||
|
||||
config = self.configuration
|
||||
self._signin(resource_path=resource_path)
|
||||
|
||||
# header parameters
|
||||
header_params = header_params or {}
|
||||
config.update_request_header_params(resource_path, header_params)
|
||||
header_params.update(self.default_headers)
|
||||
if self.cookie:
|
||||
header_params['Cookie'] = self.cookie
|
||||
if header_params:
|
||||
header_params = self.sanitize_for_serialization(header_params)
|
||||
header_params = dict(self.parameters_to_tuples(header_params,
|
||||
collection_formats))
|
||||
|
||||
# path parameters
|
||||
if path_params:
|
||||
path_params = self.sanitize_for_serialization(path_params)
|
||||
path_params = self.parameters_to_tuples(path_params,
|
||||
collection_formats)
|
||||
for k, v in path_params:
|
||||
# specified safe chars, encode everything
|
||||
resource_path = resource_path.replace(
|
||||
'{%s}' % k,
|
||||
quote(str(v), safe=config.safe_chars_for_path_param)
|
||||
)
|
||||
|
||||
# query parameters
|
||||
if query_params:
|
||||
query_params = self.sanitize_for_serialization(query_params)
|
||||
query_params = self.parameters_to_tuples(query_params,
|
||||
collection_formats)
|
||||
|
||||
# post parameters
|
||||
if post_params or files:
|
||||
post_params = self.prepare_post_parameters(post_params, files)
|
||||
post_params = self.sanitize_for_serialization(post_params)
|
||||
post_params = self.parameters_to_tuples(post_params,
|
||||
collection_formats)
|
||||
|
||||
# auth setting
|
||||
self.update_params_for_auth(header_params, query_params, auth_settings)
|
||||
|
||||
# body
|
||||
if body:
|
||||
body = self.sanitize_for_serialization(body)
|
||||
body = config.update_request_body(resource_path, body)
|
||||
|
||||
# request url
|
||||
url = self.configuration.host + resource_path
|
||||
|
||||
urlopen_kw = urlopen_kw or {}
|
||||
|
||||
# perform request and return response
|
||||
response_data = self.request(
|
||||
method, url, query_params=query_params, headers=header_params,
|
||||
post_params=post_params, body=body,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout, **urlopen_kw)
|
||||
|
||||
self.last_response = response_data
|
||||
|
||||
return_data = response_data
|
||||
if _preload_content:
|
||||
# deserialize response data
|
||||
if response_type:
|
||||
return_data = self.deserialize(response_data, response_type)
|
||||
else:
|
||||
return_data = None
|
||||
|
||||
if _return_http_data_only:
|
||||
return (return_data)
|
||||
else:
|
||||
return (return_data, response_data.status,
|
||||
response_data.getheaders())
|
||||
|
||||
def sanitize_for_serialization(self, obj):
|
||||
"""Build a JSON POST object.
|
||||
|
||||
If obj is None, return None.
|
||||
If obj is str, int, long, float, bool, return directly.
|
||||
If obj is datetime.datetime, datetime.date
|
||||
convert to string in iso8601 format.
|
||||
If obj is list, sanitize each element in the list.
|
||||
If obj is dict, return the dict.
|
||||
If obj is OpenAPI model, return the properties dict.
|
||||
|
||||
:param obj: The data to serialize.
|
||||
:return: The serialized form of data.
|
||||
"""
|
||||
if obj is None:
|
||||
return None
|
||||
elif isinstance(obj, self.PRIMITIVE_TYPES):
|
||||
return obj
|
||||
elif isinstance(obj, list):
|
||||
return [self.sanitize_for_serialization(sub_obj)
|
||||
for sub_obj in obj]
|
||||
elif isinstance(obj, tuple):
|
||||
return tuple(self.sanitize_for_serialization(sub_obj)
|
||||
for sub_obj in obj)
|
||||
elif isinstance(obj, (datetime.datetime, datetime.date)):
|
||||
return obj.isoformat()
|
||||
|
||||
if isinstance(obj, dict):
|
||||
obj_dict = obj
|
||||
else:
|
||||
# Convert model obj to dict except
|
||||
# attributes `openapi_types`, `attribute_map`
|
||||
# and attributes which value is not None.
|
||||
# Convert attribute name to json key in
|
||||
# model definition for request.
|
||||
obj_dict = {obj.attribute_map[attr]: getattr(obj, attr)
|
||||
for attr, _ in obj.openapi_types.items()
|
||||
if getattr(obj, attr) is not None}
|
||||
|
||||
return {key: self.sanitize_for_serialization(val)
|
||||
for key, val in obj_dict.items()}
|
||||
|
||||
def deserialize(self, response, response_type):
|
||||
"""Deserializes response into an object.
|
||||
|
||||
:param response: RESTResponse object to be deserialized.
|
||||
:param response_type: class literal for
|
||||
deserialized object, or string of class name.
|
||||
|
||||
:return: deserialized object.
|
||||
"""
|
||||
# handle file downloading
|
||||
# save response body into a tmp file and return the instance
|
||||
if response_type == "file":
|
||||
return self.__deserialize_file(response)
|
||||
|
||||
# fetch data from response object
|
||||
try:
|
||||
data = json.loads(response.data)
|
||||
except ValueError:
|
||||
data = response.data
|
||||
|
||||
return self.__deserialize(data, response_type)
|
||||
|
||||
def __deserialize(self, data, klass):
|
||||
"""Deserializes dict, list, str into an object.
|
||||
|
||||
:param data: dict, list or str.
|
||||
:param klass: class literal, or string of class name.
|
||||
|
||||
:return: object.
|
||||
"""
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
if type(klass) == str:
|
||||
if klass.startswith('list['):
|
||||
sub_kls = re.match(r'list\[(.*)\]', klass).group(1)
|
||||
return [self.__deserialize(sub_data, sub_kls)
|
||||
for sub_data in data]
|
||||
|
||||
if klass.startswith('dict('):
|
||||
sub_kls = re.match(r'dict\(([^,]*), (.*)\)', klass).group(2)
|
||||
return {k: self.__deserialize(v, sub_kls)
|
||||
for k, v in data.items()}
|
||||
|
||||
# convert str to class
|
||||
if klass in self.NATIVE_TYPES_MAPPING:
|
||||
klass = self.NATIVE_TYPES_MAPPING[klass]
|
||||
else:
|
||||
klass = getattr(influxdb_client.domain, klass)
|
||||
|
||||
if klass in self.PRIMITIVE_TYPES:
|
||||
return self.__deserialize_primitive(data, klass)
|
||||
elif klass == object:
|
||||
return self.__deserialize_object(data)
|
||||
elif klass == datetime.date:
|
||||
return self.__deserialize_date(data)
|
||||
elif klass == datetime.datetime:
|
||||
return self.__deserialize_datatime(data)
|
||||
else:
|
||||
return self.__deserialize_model(data, klass)
|
||||
|
||||
def call_api(self, resource_path, method,
|
||||
path_params=None, query_params=None, header_params=None,
|
||||
body=None, post_params=None, files=None,
|
||||
response_type=None, auth_settings=None, async_req=None,
|
||||
_return_http_data_only=None, collection_formats=None,
|
||||
_preload_content=True, _request_timeout=None, urlopen_kw=None):
|
||||
"""Make the HTTP request (synchronous) and Return deserialized data.
|
||||
|
||||
To make an async_req request, set the async_req parameter.
|
||||
|
||||
:param resource_path: Path to method endpoint.
|
||||
:param method: Method to call.
|
||||
:param path_params: Path parameters in the url.
|
||||
:param query_params: Query parameters in the url.
|
||||
:param header_params: Header parameters to be
|
||||
placed in the request header.
|
||||
:param body: Request body.
|
||||
:param post_params dict: Request post form parameters,
|
||||
for `application/x-www-form-urlencoded`, `multipart/form-data`.
|
||||
:param auth_settings list: Auth Settings names for the request.
|
||||
:param response: Response data type.
|
||||
:param files dict: key -> filename, value -> filepath,
|
||||
for `multipart/form-data`.
|
||||
:param async_req bool: execute request asynchronously
|
||||
:param _return_http_data_only: response data without head status code
|
||||
and headers
|
||||
:param collection_formats: dict of collection formats for path, query,
|
||||
header, and post parameters.
|
||||
:param _preload_content: if False, the urllib3.HTTPResponse object will
|
||||
be returned without reading/decoding response
|
||||
data. Default is True.
|
||||
:param _request_timeout: timeout setting for this request. If one
|
||||
number provided, it will be total request
|
||||
timeout. It can also be a pair (tuple) of
|
||||
(connection, read) timeouts.
|
||||
:param urlopen_kw: Additional parameters are passed to
|
||||
:meth:`urllib3.request.RequestMethods.request`
|
||||
:return:
|
||||
If async_req parameter is True,
|
||||
the request will be called asynchronously.
|
||||
The method will return the request thread.
|
||||
If parameter async_req is False or missing,
|
||||
then the method will return the response directly.
|
||||
"""
|
||||
if not async_req:
|
||||
return self.__call_api(resource_path, method,
|
||||
path_params, query_params, header_params,
|
||||
body, post_params, files,
|
||||
response_type, auth_settings,
|
||||
_return_http_data_only, collection_formats,
|
||||
_preload_content, _request_timeout, urlopen_kw)
|
||||
else:
|
||||
thread = self.pool.apply_async(self.__call_api, (resource_path,
|
||||
method, path_params, query_params,
|
||||
header_params, body,
|
||||
post_params, files,
|
||||
response_type, auth_settings,
|
||||
_return_http_data_only,
|
||||
collection_formats,
|
||||
_preload_content, _request_timeout, urlopen_kw))
|
||||
return thread
|
||||
|
||||
def request(self, method, url, query_params=None, headers=None,
|
||||
post_params=None, body=None, _preload_content=True,
|
||||
_request_timeout=None, **urlopen_kw):
|
||||
"""Make the HTTP request using RESTClient."""
|
||||
if method == "GET":
|
||||
return self.rest_client.GET(url,
|
||||
query_params=query_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
headers=headers,
|
||||
**urlopen_kw)
|
||||
elif method == "HEAD":
|
||||
return self.rest_client.HEAD(url,
|
||||
query_params=query_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
headers=headers,
|
||||
**urlopen_kw)
|
||||
elif method == "OPTIONS":
|
||||
return self.rest_client.OPTIONS(url,
|
||||
query_params=query_params,
|
||||
headers=headers,
|
||||
post_params=post_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body,
|
||||
**urlopen_kw)
|
||||
elif method == "POST":
|
||||
return self.rest_client.POST(url,
|
||||
query_params=query_params,
|
||||
headers=headers,
|
||||
post_params=post_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body,
|
||||
**urlopen_kw)
|
||||
elif method == "PUT":
|
||||
return self.rest_client.PUT(url,
|
||||
query_params=query_params,
|
||||
headers=headers,
|
||||
post_params=post_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body,
|
||||
**urlopen_kw)
|
||||
elif method == "PATCH":
|
||||
return self.rest_client.PATCH(url,
|
||||
query_params=query_params,
|
||||
headers=headers,
|
||||
post_params=post_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body,
|
||||
**urlopen_kw)
|
||||
elif method == "DELETE":
|
||||
return self.rest_client.DELETE(url,
|
||||
query_params=query_params,
|
||||
headers=headers,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body,
|
||||
**urlopen_kw)
|
||||
else:
|
||||
raise ValueError(
|
||||
"http method must be `GET`, `HEAD`, `OPTIONS`,"
|
||||
" `POST`, `PATCH`, `PUT` or `DELETE`."
|
||||
)
|
||||
|
||||
def parameters_to_tuples(self, params, collection_formats):
|
||||
"""Get parameters as list of tuples, formatting collections.
|
||||
|
||||
:param params: Parameters as dict or list of two-tuples
|
||||
:param dict collection_formats: Parameter collection formats
|
||||
:return: Parameters as list of tuples, collections formatted
|
||||
"""
|
||||
new_params = []
|
||||
if collection_formats is None:
|
||||
collection_formats = {}
|
||||
for k, v in params.items() if isinstance(params, dict) else params: # noqa: E501
|
||||
if k in collection_formats:
|
||||
collection_format = collection_formats[k]
|
||||
if collection_format == 'multi':
|
||||
new_params.extend((k, value) for value in v)
|
||||
else:
|
||||
if collection_format == 'ssv':
|
||||
delimiter = ' '
|
||||
elif collection_format == 'tsv':
|
||||
delimiter = '\t'
|
||||
elif collection_format == 'pipes':
|
||||
delimiter = '|'
|
||||
else: # csv is the default
|
||||
delimiter = ','
|
||||
new_params.append(
|
||||
(k, delimiter.join(str(value) for value in v)))
|
||||
else:
|
||||
new_params.append((k, v))
|
||||
return new_params
|
||||
|
||||
def prepare_post_parameters(self, post_params=None, files=None):
|
||||
"""Build form parameters.
|
||||
|
||||
:param post_params: Normal form parameters.
|
||||
:param files: File parameters.
|
||||
:return: Form parameters with files.
|
||||
"""
|
||||
params = []
|
||||
|
||||
if post_params:
|
||||
params = post_params
|
||||
|
||||
if files:
|
||||
for k, v in files.items():
|
||||
if not v:
|
||||
continue
|
||||
file_names = v if type(v) is list else [v]
|
||||
for n in file_names:
|
||||
with open(n, 'rb') as f:
|
||||
filename = os.path.basename(f.name)
|
||||
filedata = f.read()
|
||||
mimetype = (mimetypes.guess_type(filename)[0] or
|
||||
'application/octet-stream')
|
||||
params.append(
|
||||
tuple([k, tuple([filename, filedata, mimetype])]))
|
||||
|
||||
return params
|
||||
|
||||
def select_header_accept(self, accepts):
|
||||
"""Return `Accept` based on an array of accepts provided.
|
||||
|
||||
:param accepts: List of headers.
|
||||
:return: Accept (e.g. application/json).
|
||||
"""
|
||||
if not accepts:
|
||||
return
|
||||
|
||||
accepts = [x.lower() for x in accepts]
|
||||
|
||||
if 'application/json' in accepts:
|
||||
return 'application/json'
|
||||
else:
|
||||
return ', '.join(accepts)
|
||||
|
||||
def select_header_content_type(self, content_types):
|
||||
"""Return `Content-Type` based on an array of content_types provided.
|
||||
|
||||
:param content_types: List of content-types.
|
||||
:return: Content-Type (e.g. application/json).
|
||||
"""
|
||||
if not content_types:
|
||||
return 'application/json'
|
||||
|
||||
content_types = [x.lower() for x in content_types]
|
||||
|
||||
if 'application/json' in content_types or '*/*' in content_types:
|
||||
return 'application/json'
|
||||
else:
|
||||
return content_types[0]
|
||||
|
||||
def update_params_for_auth(self, headers, querys, auth_settings):
|
||||
"""Update header and query params based on authentication setting.
|
||||
|
||||
:param headers: Header parameters dict to be updated.
|
||||
:param querys: Query parameters tuple list to be updated.
|
||||
:param auth_settings: Authentication setting identifiers list.
|
||||
"""
|
||||
if not auth_settings:
|
||||
return
|
||||
|
||||
for auth in auth_settings:
|
||||
auth_setting = self.configuration.auth_settings().get(auth)
|
||||
if auth_setting:
|
||||
if not auth_setting['value']:
|
||||
continue
|
||||
elif auth_setting['in'] == 'header':
|
||||
headers[auth_setting['key']] = auth_setting['value']
|
||||
elif auth_setting['in'] == 'query':
|
||||
querys.append((auth_setting['key'], auth_setting['value']))
|
||||
else:
|
||||
raise ValueError(
|
||||
'Authentication token must be in `query` or `header`'
|
||||
)
|
||||
|
||||
def __deserialize_file(self, response):
|
||||
"""Deserializes body to file.
|
||||
|
||||
Saves response body into a file in a temporary folder,
|
||||
using the filename from the `Content-Disposition` header if provided.
|
||||
|
||||
:param response: RESTResponse.
|
||||
:return: file path.
|
||||
"""
|
||||
fd, path = tempfile.mkstemp(dir=self.configuration.temp_folder_path)
|
||||
os.close(fd)
|
||||
os.remove(path)
|
||||
|
||||
content_disposition = response.getheader("Content-Disposition")
|
||||
if content_disposition:
|
||||
filename = re.search(r'filename=[\'"]?([^\'"\s]+)[\'"]?',
|
||||
content_disposition).group(1)
|
||||
path = os.path.join(os.path.dirname(path), filename)
|
||||
|
||||
with open(path, "wb") as f:
|
||||
f.write(response.data)
|
||||
|
||||
return path
|
||||
|
||||
def __deserialize_primitive(self, data, klass):
|
||||
"""Deserializes string to primitive type.
|
||||
|
||||
:param data: str.
|
||||
:param klass: class literal.
|
||||
|
||||
:return: int, long, float, str, bool.
|
||||
"""
|
||||
try:
|
||||
return klass(data)
|
||||
except UnicodeEncodeError:
|
||||
return str(data)
|
||||
except TypeError:
|
||||
return data
|
||||
|
||||
def __deserialize_object(self, value):
|
||||
"""Return an original value.
|
||||
|
||||
:return: object.
|
||||
"""
|
||||
return value
|
||||
|
||||
def __deserialize_date(self, string):
|
||||
"""Deserializes string to date.
|
||||
|
||||
:param string: str.
|
||||
:return: date.
|
||||
"""
|
||||
try:
|
||||
from dateutil.parser import parse
|
||||
return parse(string).date()
|
||||
except ImportError:
|
||||
return string
|
||||
except ValueError:
|
||||
raise rest.ApiException(
|
||||
status=0,
|
||||
reason="Failed to parse `{0}` as date object".format(string)
|
||||
)
|
||||
|
||||
def __deserialize_datatime(self, string):
|
||||
"""Deserializes string to datetime.
|
||||
|
||||
The string should be in iso8601 datetime format.
|
||||
|
||||
:param string: str.
|
||||
:return: datetime.
|
||||
"""
|
||||
try:
|
||||
from dateutil.parser import parse
|
||||
return parse(string)
|
||||
except ImportError:
|
||||
return string
|
||||
except ValueError:
|
||||
raise rest.ApiException(
|
||||
status=0,
|
||||
reason=(
|
||||
"Failed to parse `{0}` as datetime object"
|
||||
.format(string)
|
||||
)
|
||||
)
|
||||
|
||||
def __deserialize_model(self, data, klass):
|
||||
"""Deserializes list or dict to model.
|
||||
|
||||
:param data: dict, list.
|
||||
:param klass: class literal.
|
||||
:return: model object.
|
||||
"""
|
||||
if not klass.openapi_types and not hasattr(klass,
|
||||
'get_real_child_model'):
|
||||
return data
|
||||
|
||||
kwargs = {}
|
||||
if klass.openapi_types is not None:
|
||||
for attr, attr_type in klass.openapi_types.items():
|
||||
if (data is not None and
|
||||
klass.attribute_map[attr] in data and
|
||||
isinstance(data, (list, dict))):
|
||||
value = data[klass.attribute_map[attr]]
|
||||
kwargs[attr] = self.__deserialize(value, attr_type)
|
||||
|
||||
instance = klass(**kwargs)
|
||||
|
||||
if hasattr(instance, 'get_real_child_model'):
|
||||
klass_name = instance.get_real_child_model(data)
|
||||
if klass_name:
|
||||
instance = self.__deserialize(data, klass_name)
|
||||
return instance
|
||||
|
||||
def _signin(self, resource_path: str):
|
||||
if _requires_create_user_session(self.configuration, self.cookie, resource_path):
|
||||
http_info = SigninService(self).post_signin_with_http_info()
|
||||
self.cookie = http_info[2]['set-cookie']
|
||||
|
||||
def _signout(self):
|
||||
if _requires_expire_user_session(self.configuration, self.cookie):
|
||||
SignoutService(self).post_signout()
|
||||
self.cookie = None
|
||||
@@ -0,0 +1,355 @@
|
||||
# coding: utf-8
|
||||
|
||||
"""
|
||||
InfluxDB OSS API Service.
|
||||
|
||||
The InfluxDB v2 API provides a programmatic interface for all interactions with InfluxDB. Access the InfluxDB API using the `/api/v2/` endpoint. # noqa: E501
|
||||
|
||||
OpenAPI spec version: 2.0.0
|
||||
Generated by: https://openapi-generator.tech
|
||||
"""
|
||||
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
import ssl
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from influxdb_client.rest import ApiException
|
||||
from influxdb_client.rest import _BaseRESTClient
|
||||
|
||||
try:
|
||||
import urllib3
|
||||
except ImportError:
|
||||
raise ImportError('OpenAPI Python client requires urllib3.')
|
||||
|
||||
|
||||
class RESTResponse(io.IOBase):
|
||||
"""NOTE: This class is auto generated by OpenAPI Generator.
|
||||
|
||||
Ref: https://openapi-generator.tech
|
||||
Do not edit the class manually.
|
||||
"""
|
||||
|
||||
def __init__(self, resp):
|
||||
"""Initialize with HTTP response."""
|
||||
self.urllib3_response = resp
|
||||
self.status = resp.status
|
||||
self.reason = resp.reason
|
||||
self.data = resp.data
|
||||
|
||||
def getheaders(self):
|
||||
"""Return a dictionary of the response headers."""
|
||||
return self.urllib3_response.headers
|
||||
|
||||
def getheader(self, name, default=None):
|
||||
"""Return a given response header."""
|
||||
return self.urllib3_response.headers.get(name, default)
|
||||
|
||||
|
||||
class RESTClientObject(object):
|
||||
"""NOTE: This class is auto generated by OpenAPI Generator.
|
||||
|
||||
Ref: https://openapi-generator.tech
|
||||
Do not edit the class manually.
|
||||
"""
|
||||
|
||||
def __init__(self, configuration, pools_size=4, maxsize=None, retries=False):
|
||||
"""Initialize REST client."""
|
||||
# urllib3.PoolManager will pass all kw parameters to connectionpool
|
||||
# https://github.com/shazow/urllib3/blob/f9409436f83aeb79fbaf090181cd81b784f1b8ce/urllib3/poolmanager.py#L75 # noqa: E501
|
||||
# https://github.com/shazow/urllib3/blob/f9409436f83aeb79fbaf090181cd81b784f1b8ce/urllib3/connectionpool.py#L680 # noqa: E501
|
||||
# maxsize is the number of requests to host that are allowed in parallel # noqa: E501
|
||||
# Custom SSL certificates and client certificates: http://urllib3.readthedocs.io/en/latest/advanced-usage.html # noqa: E501
|
||||
|
||||
self.configuration = configuration
|
||||
self.pools_size = pools_size
|
||||
self.maxsize = maxsize
|
||||
self.retries = retries
|
||||
|
||||
# cert_reqs
|
||||
if configuration.verify_ssl:
|
||||
cert_reqs = ssl.CERT_REQUIRED
|
||||
else:
|
||||
cert_reqs = ssl.CERT_NONE
|
||||
|
||||
# ca_certs
|
||||
if configuration.ssl_ca_cert:
|
||||
ca_certs = configuration.ssl_ca_cert
|
||||
else:
|
||||
ca_certs = None
|
||||
|
||||
addition_pool_args = {}
|
||||
if configuration.assert_hostname is not None:
|
||||
addition_pool_args['assert_hostname'] = configuration.assert_hostname # noqa: E501
|
||||
addition_pool_args['retries'] = self.retries
|
||||
|
||||
if maxsize is None:
|
||||
if configuration.connection_pool_maxsize is not None:
|
||||
maxsize = configuration.connection_pool_maxsize
|
||||
else:
|
||||
maxsize = 4
|
||||
|
||||
# https pool manager
|
||||
if configuration.proxy:
|
||||
self.pool_manager = urllib3.ProxyManager(
|
||||
num_pools=pools_size,
|
||||
maxsize=maxsize,
|
||||
cert_reqs=cert_reqs,
|
||||
ca_certs=ca_certs,
|
||||
cert_file=configuration.cert_file,
|
||||
key_file=configuration.cert_key_file,
|
||||
key_password=configuration.cert_key_password,
|
||||
proxy_url=configuration.proxy,
|
||||
proxy_headers=configuration.proxy_headers,
|
||||
ssl_context=configuration.ssl_context,
|
||||
**addition_pool_args
|
||||
)
|
||||
else:
|
||||
self.pool_manager = urllib3.PoolManager(
|
||||
num_pools=pools_size,
|
||||
maxsize=maxsize,
|
||||
cert_reqs=cert_reqs,
|
||||
ca_certs=ca_certs,
|
||||
cert_file=configuration.cert_file,
|
||||
key_file=configuration.cert_key_file,
|
||||
key_password=configuration.cert_key_password,
|
||||
ssl_context=configuration.ssl_context,
|
||||
**addition_pool_args
|
||||
)
|
||||
|
||||
def request(self, method, url, query_params=None, headers=None,
|
||||
body=None, post_params=None, _preload_content=True,
|
||||
_request_timeout=None, **urlopen_kw):
|
||||
"""Perform requests.
|
||||
|
||||
:param method: http request method
|
||||
:param url: http request url
|
||||
:param query_params: query parameters in the url
|
||||
:param headers: http request headers
|
||||
:param body: request json body, for `application/json`
|
||||
:param post_params: request post parameters,
|
||||
`application/x-www-form-urlencoded`
|
||||
and `multipart/form-data`
|
||||
:param _preload_content: if False, the urllib3.HTTPResponse object will
|
||||
be returned without reading/decoding response
|
||||
data. Default is True.
|
||||
:param _request_timeout: timeout setting for this request. If one
|
||||
number provided, it will be total request
|
||||
timeout. It can also be a pair (tuple) of
|
||||
(connection, read) timeouts.
|
||||
:param urlopen_kw: Additional parameters are passed to
|
||||
:meth:`urllib3.request.RequestMethods.request`
|
||||
"""
|
||||
method = method.upper()
|
||||
assert method in ['GET', 'HEAD', 'DELETE', 'POST', 'PUT',
|
||||
'PATCH', 'OPTIONS']
|
||||
|
||||
if post_params and body:
|
||||
raise ValueError(
|
||||
"body parameter cannot be used with post_params parameter."
|
||||
)
|
||||
|
||||
post_params = post_params or {}
|
||||
headers = headers or {}
|
||||
|
||||
timeout = None
|
||||
_configured_timeout = _request_timeout or self.configuration.timeout
|
||||
if _configured_timeout:
|
||||
if isinstance(_configured_timeout, (int, float, )): # noqa: E501,F821
|
||||
timeout = urllib3.Timeout(total=_configured_timeout / 1_000)
|
||||
elif (isinstance(_configured_timeout, tuple) and
|
||||
len(_configured_timeout) == 2):
|
||||
timeout = urllib3.Timeout(
|
||||
connect=_configured_timeout[0] / 1_000, read=_configured_timeout[1] / 1_000)
|
||||
|
||||
if 'Content-Type' not in headers:
|
||||
headers['Content-Type'] = 'application/json'
|
||||
|
||||
if self.configuration.debug:
|
||||
_BaseRESTClient.log_request(method, f"{url}{'' if query_params is None else '?' + urlencode(query_params)}")
|
||||
_BaseRESTClient.log_headers(headers, '>>>')
|
||||
_BaseRESTClient.log_body(body, '>>>')
|
||||
|
||||
try:
|
||||
# For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE`
|
||||
if method in ['POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE']:
|
||||
if query_params:
|
||||
url += '?' + urlencode(query_params)
|
||||
if re.search('json', headers['Content-Type'], re.IGNORECASE):
|
||||
request_body = None
|
||||
if body is not None:
|
||||
request_body = json.dumps(body)
|
||||
r = self.pool_manager.request(
|
||||
method, url,
|
||||
body=request_body,
|
||||
preload_content=_preload_content,
|
||||
timeout=timeout,
|
||||
headers=headers,
|
||||
**urlopen_kw)
|
||||
elif headers['Content-Type'] == 'application/x-www-form-urlencoded': # noqa: E501
|
||||
r = self.pool_manager.request(
|
||||
method, url,
|
||||
fields=post_params,
|
||||
encode_multipart=False,
|
||||
preload_content=_preload_content,
|
||||
timeout=timeout,
|
||||
headers=headers,
|
||||
**urlopen_kw)
|
||||
elif headers['Content-Type'] == 'multipart/form-data':
|
||||
# must del headers['Content-Type'], or the correct
|
||||
# Content-Type which generated by urllib3 will be
|
||||
# overwritten.
|
||||
del headers['Content-Type']
|
||||
r = self.pool_manager.request(
|
||||
method, url,
|
||||
fields=post_params,
|
||||
encode_multipart=True,
|
||||
preload_content=_preload_content,
|
||||
timeout=timeout,
|
||||
headers=headers,
|
||||
**urlopen_kw)
|
||||
# Pass a `string` parameter directly in the body to support
|
||||
# other content types than Json when `body` argument is
|
||||
# provided in serialized form
|
||||
elif isinstance(body, str) or isinstance(body, bytes):
|
||||
request_body = body
|
||||
r = self.pool_manager.request(
|
||||
method, url,
|
||||
body=request_body,
|
||||
preload_content=_preload_content,
|
||||
timeout=timeout,
|
||||
headers=headers,
|
||||
**urlopen_kw)
|
||||
else:
|
||||
# Cannot generate the request from given parameters
|
||||
msg = """Cannot prepare a request message for provided
|
||||
arguments. Please check that your arguments match
|
||||
declared content type."""
|
||||
raise ApiException(status=0, reason=msg)
|
||||
# For `GET`, `HEAD`
|
||||
else:
|
||||
r = self.pool_manager.request(method, url,
|
||||
fields=query_params,
|
||||
preload_content=_preload_content,
|
||||
timeout=timeout,
|
||||
headers=headers,
|
||||
**urlopen_kw)
|
||||
except urllib3.exceptions.SSLError as e:
|
||||
msg = "{0}\n{1}".format(type(e).__name__, str(e))
|
||||
raise ApiException(status=0, reason=msg)
|
||||
|
||||
if _preload_content:
|
||||
r = RESTResponse(r)
|
||||
|
||||
# In the python 3, the response.data is bytes.
|
||||
# we need to decode it to string.
|
||||
r.data = r.data.decode('utf8')
|
||||
|
||||
if self.configuration.debug:
|
||||
_BaseRESTClient.log_response(r.status)
|
||||
if hasattr(r, 'headers'):
|
||||
_BaseRESTClient.log_headers(r.headers, '<<<')
|
||||
if hasattr(r, 'urllib3_response'):
|
||||
_BaseRESTClient.log_headers(r.urllib3_response.headers, '<<<')
|
||||
_BaseRESTClient.log_body(r.data, '<<<')
|
||||
|
||||
if not 200 <= r.status <= 299:
|
||||
raise ApiException(http_resp=r)
|
||||
|
||||
return r
|
||||
|
||||
def GET(self, url, headers=None, query_params=None, _preload_content=True,
|
||||
_request_timeout=None, **urlopen_kw):
|
||||
"""Perform GET HTTP request."""
|
||||
return self.request("GET", url,
|
||||
headers=headers,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
query_params=query_params,
|
||||
**urlopen_kw)
|
||||
|
||||
def HEAD(self, url, headers=None, query_params=None, _preload_content=True,
|
||||
_request_timeout=None, **urlopen_kw):
|
||||
"""Perform HEAD HTTP request."""
|
||||
return self.request("HEAD", url,
|
||||
headers=headers,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
query_params=query_params,
|
||||
**urlopen_kw)
|
||||
|
||||
def OPTIONS(self, url, headers=None, query_params=None, post_params=None,
|
||||
body=None, _preload_content=True, _request_timeout=None, **urlopen_kw):
|
||||
"""Perform OPTIONS HTTP request."""
|
||||
return self.request("OPTIONS", url,
|
||||
headers=headers,
|
||||
query_params=query_params,
|
||||
post_params=post_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body,
|
||||
**urlopen_kw)
|
||||
|
||||
def DELETE(self, url, headers=None, query_params=None, body=None,
|
||||
_preload_content=True, _request_timeout=None, **urlopen_kw):
|
||||
"""Perform DELETE HTTP request."""
|
||||
return self.request("DELETE", url,
|
||||
headers=headers,
|
||||
query_params=query_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body,
|
||||
**urlopen_kw)
|
||||
|
||||
def POST(self, url, headers=None, query_params=None, post_params=None,
|
||||
body=None, _preload_content=True, _request_timeout=None, **urlopen_kw):
|
||||
"""Perform POST HTTP request."""
|
||||
return self.request("POST", url,
|
||||
headers=headers,
|
||||
query_params=query_params,
|
||||
post_params=post_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body,
|
||||
**urlopen_kw)
|
||||
|
||||
def PUT(self, url, headers=None, query_params=None, post_params=None,
|
||||
body=None, _preload_content=True, _request_timeout=None, **urlopen_kw):
|
||||
"""Perform PUT HTTP request."""
|
||||
return self.request("PUT", url,
|
||||
headers=headers,
|
||||
query_params=query_params,
|
||||
post_params=post_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body,
|
||||
**urlopen_kw)
|
||||
|
||||
def PATCH(self, url, headers=None, query_params=None, post_params=None,
|
||||
body=None, _preload_content=True, _request_timeout=None, **urlopen_kw):
|
||||
"""Perform PATCH HTTP request."""
|
||||
return self.request("PATCH", url,
|
||||
headers=headers,
|
||||
query_params=query_params,
|
||||
post_params=post_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body,
|
||||
**urlopen_kw)
|
||||
|
||||
def __getstate__(self):
|
||||
"""Return a dict of attributes that you want to pickle."""
|
||||
state = self.__dict__.copy()
|
||||
# Remove Pool managaer
|
||||
del state['pool_manager']
|
||||
return state
|
||||
|
||||
def __setstate__(self, state):
|
||||
"""Set your object with the provided dict."""
|
||||
self.__dict__.update(state)
|
||||
# Init Pool manager
|
||||
self.__init__(self.configuration, self.pools_size, self.maxsize, self.retries)
|
||||
@@ -0,0 +1,56 @@
|
||||
# flake8: noqa
|
||||
|
||||
"""
|
||||
InfluxDB OSS API Service.
|
||||
|
||||
The InfluxDB v2 API provides a programmatic interface for all interactions with InfluxDB. Access the InfluxDB API using the `/api/v2/` endpoint. # noqa: E501
|
||||
|
||||
OpenAPI spec version: 2.0.0
|
||||
Generated by: https://openapi-generator.tech
|
||||
"""
|
||||
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
# import apis into api package
|
||||
from influxdb_client.service.authorizations_service import AuthorizationsService
|
||||
from influxdb_client.service.backup_service import BackupService
|
||||
from influxdb_client.service.bucket_schemas_service import BucketSchemasService
|
||||
from influxdb_client.service.buckets_service import BucketsService
|
||||
from influxdb_client.service.cells_service import CellsService
|
||||
from influxdb_client.service.checks_service import ChecksService
|
||||
from influxdb_client.service.config_service import ConfigService
|
||||
from influxdb_client.service.dbr_ps_service import DBRPsService
|
||||
from influxdb_client.service.dashboards_service import DashboardsService
|
||||
from influxdb_client.service.delete_service import DeleteService
|
||||
from influxdb_client.service.health_service import HealthService
|
||||
from influxdb_client.service.invokable_scripts_service import InvokableScriptsService
|
||||
from influxdb_client.service.labels_service import LabelsService
|
||||
from influxdb_client.service.legacy_authorizations_service import LegacyAuthorizationsService
|
||||
from influxdb_client.service.metrics_service import MetricsService
|
||||
from influxdb_client.service.notification_endpoints_service import NotificationEndpointsService
|
||||
from influxdb_client.service.notification_rules_service import NotificationRulesService
|
||||
from influxdb_client.service.organizations_service import OrganizationsService
|
||||
from influxdb_client.service.ping_service import PingService
|
||||
from influxdb_client.service.query_service import QueryService
|
||||
from influxdb_client.service.ready_service import ReadyService
|
||||
from influxdb_client.service.remote_connections_service import RemoteConnectionsService
|
||||
from influxdb_client.service.replications_service import ReplicationsService
|
||||
from influxdb_client.service.resources_service import ResourcesService
|
||||
from influxdb_client.service.restore_service import RestoreService
|
||||
from influxdb_client.service.routes_service import RoutesService
|
||||
from influxdb_client.service.rules_service import RulesService
|
||||
from influxdb_client.service.scraper_targets_service import ScraperTargetsService
|
||||
from influxdb_client.service.secrets_service import SecretsService
|
||||
from influxdb_client.service.setup_service import SetupService
|
||||
from influxdb_client.service.signin_service import SigninService
|
||||
from influxdb_client.service.signout_service import SignoutService
|
||||
from influxdb_client.service.sources_service import SourcesService
|
||||
from influxdb_client.service.tasks_service import TasksService
|
||||
from influxdb_client.service.telegraf_plugins_service import TelegrafPluginsService
|
||||
from influxdb_client.service.telegrafs_service import TelegrafsService
|
||||
from influxdb_client.service.templates_service import TemplatesService
|
||||
from influxdb_client.service.users_service import UsersService
|
||||
from influxdb_client.service.variables_service import VariablesService
|
||||
from influxdb_client.service.views_service import ViewsService
|
||||
from influxdb_client.service.write_service import WriteService
|
||||
@@ -0,0 +1,556 @@
|
||||
"""Commons function for Sync and Async client."""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import base64
|
||||
import configparser
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Generator, Any, Union, Iterable, AsyncGenerator
|
||||
|
||||
from urllib3 import HTTPResponse
|
||||
|
||||
from influxdb_client import Configuration, Dialect, Query, OptionStatement, VariableAssignment, Identifier, \
|
||||
Expression, BooleanLiteral, IntegerLiteral, FloatLiteral, DateTimeLiteral, UnaryExpression, DurationLiteral, \
|
||||
Duration, StringLiteral, ArrayExpression, ImportDeclaration, MemberExpression, MemberAssignment, File, \
|
||||
WriteService, QueryService, DeleteService, DeletePredicateRequest
|
||||
from influxdb_client.client.flux_csv_parser import FluxResponseMetadataMode, FluxCsvParser, FluxSerializationMode
|
||||
from influxdb_client.client.flux_table import FluxRecord, TableList, CSVIterator
|
||||
from influxdb_client.client.util.date_utils import get_date_helper
|
||||
from influxdb_client.client.util.helpers import get_org_query_param
|
||||
from influxdb_client.client.warnings import MissingPivotFunction
|
||||
from influxdb_client.client.write.dataframe_serializer import DataframeSerializer
|
||||
from influxdb_client.rest import _UTF_8_encoding
|
||||
|
||||
try:
|
||||
import dataclasses
|
||||
|
||||
_HAS_DATACLASS = True
|
||||
except ModuleNotFoundError:
|
||||
_HAS_DATACLASS = False
|
||||
|
||||
LOGGERS_NAMES = [
|
||||
'influxdb_client.client.influxdb_client',
|
||||
'influxdb_client.client.influxdb_client_async',
|
||||
'influxdb_client.client.write_api',
|
||||
'influxdb_client.client.write_api_async',
|
||||
'influxdb_client.client.write.retry',
|
||||
'influxdb_client.client.write.dataframe_serializer',
|
||||
'influxdb_client.client.util.multiprocessing_helper',
|
||||
'influxdb_client.client.http',
|
||||
'influxdb_client.client.exceptions',
|
||||
]
|
||||
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
class _BaseClient(object):
|
||||
def __init__(self, url, token, debug=None, timeout=10_000, enable_gzip=False, org: str = None,
|
||||
default_tags: dict = None, http_client_logger: str = None, **kwargs) -> None:
|
||||
self.url = url
|
||||
self.token = token
|
||||
self.org = org
|
||||
|
||||
self.default_tags = default_tags
|
||||
|
||||
self.conf = _Configuration()
|
||||
if not isinstance(self.url, str):
|
||||
raise ValueError('"url" attribute is not str instance')
|
||||
if self.url.endswith("/"):
|
||||
self.conf.host = self.url[:-1]
|
||||
else:
|
||||
self.conf.host = self.url
|
||||
self.conf.enable_gzip = enable_gzip
|
||||
self.conf.verify_ssl = kwargs.get('verify_ssl', True)
|
||||
self.conf.ssl_ca_cert = kwargs.get('ssl_ca_cert', None)
|
||||
self.conf.cert_file = kwargs.get('cert_file', None)
|
||||
self.conf.cert_key_file = kwargs.get('cert_key_file', None)
|
||||
self.conf.cert_key_password = kwargs.get('cert_key_password', None)
|
||||
self.conf.ssl_context = kwargs.get('ssl_context', None)
|
||||
self.conf.proxy = kwargs.get('proxy', None)
|
||||
self.conf.proxy_headers = kwargs.get('proxy_headers', None)
|
||||
self.conf.connection_pool_maxsize = kwargs.get('connection_pool_maxsize', self.conf.connection_pool_maxsize)
|
||||
self.conf.timeout = timeout
|
||||
# logging
|
||||
self.conf.loggers["http_client_logger"] = logging.getLogger(http_client_logger)
|
||||
for client_logger in LOGGERS_NAMES:
|
||||
self.conf.loggers[client_logger] = logging.getLogger(client_logger)
|
||||
self.conf.debug = debug
|
||||
|
||||
self.conf.username = kwargs.get('username', None)
|
||||
self.conf.password = kwargs.get('password', None)
|
||||
# defaults
|
||||
self.auth_header_name = None
|
||||
self.auth_header_value = None
|
||||
# by token
|
||||
if self.token:
|
||||
self.auth_header_name = "Authorization"
|
||||
self.auth_header_value = "Token " + self.token
|
||||
# by HTTP basic
|
||||
auth_basic = kwargs.get('auth_basic', False)
|
||||
if auth_basic:
|
||||
self.auth_header_name = "Authorization"
|
||||
self.auth_header_value = "Basic " + base64.b64encode(token.encode()).decode()
|
||||
# by username, password
|
||||
if self.conf.username and self.conf.password:
|
||||
self.auth_header_name = None
|
||||
self.auth_header_value = None
|
||||
|
||||
self.retries = kwargs.get('retries', False)
|
||||
|
||||
self.profilers = kwargs.get('profilers', None)
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def _from_config_file(cls, config_file: str = "config.ini", debug=None, enable_gzip=False, **kwargs):
|
||||
config = configparser.ConfigParser()
|
||||
config_name = kwargs.get('config_name', 'influx2')
|
||||
is_json = False
|
||||
try:
|
||||
config.read(config_file)
|
||||
except configparser.ParsingError:
|
||||
with open(config_file) as json_file:
|
||||
import json
|
||||
config = json.load(json_file)
|
||||
is_json = True
|
||||
|
||||
def _config_value(key: str):
|
||||
value = str(config[key]) if is_json else config[config_name][key]
|
||||
return value.strip('"')
|
||||
|
||||
def _has_option(key: str):
|
||||
return key in config if is_json else config.has_option(config_name, key)
|
||||
|
||||
def _has_section(key: str):
|
||||
return key in config if is_json else config.has_section(key)
|
||||
|
||||
url = _config_value('url')
|
||||
token = _config_value('token')
|
||||
|
||||
timeout = None
|
||||
if _has_option('timeout'):
|
||||
timeout = _config_value('timeout')
|
||||
|
||||
org = None
|
||||
if _has_option('org'):
|
||||
org = _config_value('org')
|
||||
|
||||
verify_ssl = True
|
||||
if _has_option('verify_ssl'):
|
||||
verify_ssl = _config_value('verify_ssl')
|
||||
|
||||
ssl_ca_cert = None
|
||||
if _has_option('ssl_ca_cert'):
|
||||
ssl_ca_cert = _config_value('ssl_ca_cert')
|
||||
|
||||
cert_file = None
|
||||
if _has_option('cert_file'):
|
||||
cert_file = _config_value('cert_file')
|
||||
|
||||
cert_key_file = None
|
||||
if _has_option('cert_key_file'):
|
||||
cert_key_file = _config_value('cert_key_file')
|
||||
|
||||
cert_key_password = None
|
||||
if _has_option('cert_key_password'):
|
||||
cert_key_password = _config_value('cert_key_password')
|
||||
|
||||
connection_pool_maxsize = None
|
||||
if _has_option('connection_pool_maxsize'):
|
||||
connection_pool_maxsize = _config_value('connection_pool_maxsize')
|
||||
|
||||
auth_basic = False
|
||||
if _has_option('auth_basic'):
|
||||
auth_basic = _config_value('auth_basic')
|
||||
|
||||
default_tags = None
|
||||
if _has_section('tags'):
|
||||
if is_json:
|
||||
default_tags = config['tags']
|
||||
else:
|
||||
tags = {k: v.strip('"') for k, v in config.items('tags')}
|
||||
default_tags = dict(tags)
|
||||
|
||||
profilers = None
|
||||
if _has_option('profilers'):
|
||||
profilers = [x.strip() for x in _config_value('profilers').split(',')]
|
||||
|
||||
proxy = None
|
||||
if _has_option('proxy'):
|
||||
proxy = _config_value('proxy')
|
||||
|
||||
return cls(url, token, debug=debug, timeout=_to_int(timeout), org=org, default_tags=default_tags,
|
||||
enable_gzip=enable_gzip, verify_ssl=_to_bool(verify_ssl), ssl_ca_cert=ssl_ca_cert,
|
||||
cert_file=cert_file, cert_key_file=cert_key_file, cert_key_password=cert_key_password,
|
||||
connection_pool_maxsize=_to_int(connection_pool_maxsize), auth_basic=_to_bool(auth_basic),
|
||||
profilers=profilers, proxy=proxy, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def _from_env_properties(cls, debug=None, enable_gzip=False, **kwargs):
|
||||
url = os.getenv('INFLUXDB_V2_URL', "http://localhost:8086")
|
||||
token = os.getenv('INFLUXDB_V2_TOKEN', "my-token")
|
||||
timeout = os.getenv('INFLUXDB_V2_TIMEOUT', "10000")
|
||||
org = os.getenv('INFLUXDB_V2_ORG', "my-org")
|
||||
verify_ssl = os.getenv('INFLUXDB_V2_VERIFY_SSL', "True")
|
||||
ssl_ca_cert = os.getenv('INFLUXDB_V2_SSL_CA_CERT', None)
|
||||
cert_file = os.getenv('INFLUXDB_V2_CERT_FILE', None)
|
||||
cert_key_file = os.getenv('INFLUXDB_V2_CERT_KEY_FILE', None)
|
||||
cert_key_password = os.getenv('INFLUXDB_V2_CERT_KEY_PASSWORD', None)
|
||||
connection_pool_maxsize = os.getenv('INFLUXDB_V2_CONNECTION_POOL_MAXSIZE', None)
|
||||
auth_basic = os.getenv('INFLUXDB_V2_AUTH_BASIC', "False")
|
||||
|
||||
prof = os.getenv("INFLUXDB_V2_PROFILERS", None)
|
||||
profilers = None
|
||||
if prof is not None:
|
||||
profilers = [x.strip() for x in prof.split(',')]
|
||||
|
||||
default_tags = dict()
|
||||
|
||||
for key, value in os.environ.items():
|
||||
if key.startswith("INFLUXDB_V2_TAG_"):
|
||||
default_tags[key[16:].lower()] = value
|
||||
|
||||
return cls(url, token, debug=debug, timeout=_to_int(timeout), org=org, default_tags=default_tags,
|
||||
enable_gzip=enable_gzip, verify_ssl=_to_bool(verify_ssl), ssl_ca_cert=ssl_ca_cert,
|
||||
cert_file=cert_file, cert_key_file=cert_key_file, cert_key_password=cert_key_password,
|
||||
connection_pool_maxsize=_to_int(connection_pool_maxsize), auth_basic=_to_bool(auth_basic),
|
||||
profilers=profilers, **kwargs)
|
||||
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
class _BaseQueryApi(object):
|
||||
default_dialect = Dialect(header=True, delimiter=",", comment_prefix="#",
|
||||
annotations=["datatype", "group", "default"], date_time_format="RFC3339")
|
||||
|
||||
def __init__(self, influxdb_client, query_options=None):
|
||||
from influxdb_client.client.query_api import QueryOptions
|
||||
self._query_options = QueryOptions() if query_options is None else query_options
|
||||
self._influxdb_client = influxdb_client
|
||||
self._query_api = QueryService(influxdb_client.api_client)
|
||||
|
||||
"""Base implementation for Queryable API."""
|
||||
|
||||
def _to_tables(self, response, query_options=None, response_metadata_mode:
|
||||
FluxResponseMetadataMode = FluxResponseMetadataMode.full) -> TableList:
|
||||
"""
|
||||
Parse HTTP response to TableList.
|
||||
|
||||
:param response: HTTP response from an HTTP client. Expected type: `urllib3.response.HTTPResponse`.
|
||||
"""
|
||||
_parser = self._to_tables_parser(response, query_options, response_metadata_mode)
|
||||
list(_parser.generator())
|
||||
return _parser.table_list()
|
||||
|
||||
async def _to_tables_async(self, response, query_options=None, response_metadata_mode:
|
||||
FluxResponseMetadataMode = FluxResponseMetadataMode.full) -> TableList:
|
||||
"""
|
||||
Parse HTTP response to TableList.
|
||||
|
||||
:param response: HTTP response from an HTTP client. Expected type: `aiohttp.client_reqrep.ClientResponse`.
|
||||
"""
|
||||
async with self._to_tables_parser(response, query_options, response_metadata_mode) as parser:
|
||||
async for _ in parser.generator_async():
|
||||
pass
|
||||
return parser.table_list()
|
||||
|
||||
def _to_csv(self, response: HTTPResponse) -> CSVIterator:
|
||||
"""Parse HTTP response to CSV."""
|
||||
return CSVIterator(response)
|
||||
|
||||
def _to_flux_record_stream(self, response, query_options=None,
|
||||
response_metadata_mode: FluxResponseMetadataMode = FluxResponseMetadataMode.full) -> \
|
||||
Generator[FluxRecord, Any, None]:
|
||||
"""
|
||||
Parse HTTP response to FluxRecord stream.
|
||||
|
||||
:param response: HTTP response from an HTTP client. Expected type: `urllib3.response.HTTPResponse`.
|
||||
"""
|
||||
_parser = self._to_flux_record_stream_parser(query_options, response, response_metadata_mode)
|
||||
return _parser.generator()
|
||||
|
||||
async def _to_flux_record_stream_async(self, response, query_options=None, response_metadata_mode:
|
||||
FluxResponseMetadataMode = FluxResponseMetadataMode.full) -> \
|
||||
AsyncGenerator['FluxRecord', None]:
|
||||
"""
|
||||
Parse HTTP response to FluxRecord stream.
|
||||
|
||||
:param response: HTTP response from an HTTP client. Expected type: `aiohttp.client_reqrep.ClientResponse`.
|
||||
"""
|
||||
_parser = self._to_flux_record_stream_parser(query_options, response, response_metadata_mode)
|
||||
return (await _parser.__aenter__()).generator_async()
|
||||
|
||||
def _to_data_frame_stream(self, data_frame_index, response, query_options=None,
|
||||
response_metadata_mode: FluxResponseMetadataMode = FluxResponseMetadataMode.full,
|
||||
use_extension_dtypes=False):
|
||||
"""
|
||||
Parse HTTP response to DataFrame stream.
|
||||
|
||||
:param response: HTTP response from an HTTP client. Expected type: `urllib3.response.HTTPResponse`.
|
||||
"""
|
||||
_parser = self._to_data_frame_stream_parser(data_frame_index, query_options, response, response_metadata_mode,
|
||||
use_extension_dtypes)
|
||||
return _parser.generator()
|
||||
|
||||
async def _to_data_frame_stream_async(self, data_frame_index, response, query_options=None, response_metadata_mode:
|
||||
FluxResponseMetadataMode = FluxResponseMetadataMode.full,
|
||||
use_extension_dtypes=False):
|
||||
"""
|
||||
Parse HTTP response to DataFrame stream.
|
||||
|
||||
:param response: HTTP response from an HTTP client. Expected type: `aiohttp.client_reqrep.ClientResponse`.
|
||||
"""
|
||||
_parser = self._to_data_frame_stream_parser(data_frame_index, query_options, response, response_metadata_mode,
|
||||
use_extension_dtypes)
|
||||
return (await _parser.__aenter__()).generator_async()
|
||||
|
||||
def _to_tables_parser(self, response, query_options, response_metadata_mode):
|
||||
return FluxCsvParser(response=response, serialization_mode=FluxSerializationMode.tables,
|
||||
query_options=query_options, response_metadata_mode=response_metadata_mode)
|
||||
|
||||
def _to_flux_record_stream_parser(self, query_options, response, response_metadata_mode):
|
||||
return FluxCsvParser(response=response, serialization_mode=FluxSerializationMode.stream,
|
||||
query_options=query_options, response_metadata_mode=response_metadata_mode)
|
||||
|
||||
def _to_data_frame_stream_parser(self, data_frame_index, query_options, response, response_metadata_mode,
|
||||
use_extension_dtypes):
|
||||
return FluxCsvParser(response=response, serialization_mode=FluxSerializationMode.dataFrame,
|
||||
data_frame_index=data_frame_index, query_options=query_options,
|
||||
response_metadata_mode=response_metadata_mode,
|
||||
use_extension_dtypes=use_extension_dtypes)
|
||||
|
||||
def _to_data_frames(self, _generator):
|
||||
"""Parse stream of DataFrames into expected type."""
|
||||
from ..extras import pd
|
||||
if isinstance(_generator, list):
|
||||
_dataFrames = _generator
|
||||
else:
|
||||
_dataFrames = list(_generator)
|
||||
|
||||
if len(_dataFrames) == 0:
|
||||
return pd.DataFrame(columns=[], index=None)
|
||||
elif len(_dataFrames) == 1:
|
||||
return _dataFrames[0]
|
||||
else:
|
||||
return _dataFrames
|
||||
|
||||
def _org_param(self, org):
|
||||
return get_org_query_param(org=org, client=self._influxdb_client)
|
||||
|
||||
def _get_query_options(self):
|
||||
if self._query_options and self._query_options.profilers:
|
||||
return self._query_options
|
||||
elif self._influxdb_client.profilers:
|
||||
from influxdb_client.client.query_api import QueryOptions
|
||||
return QueryOptions(profilers=self._influxdb_client.profilers)
|
||||
|
||||
def _create_query(self, query, dialect=default_dialect, params: dict = None, **kwargs):
|
||||
query_options = self._get_query_options()
|
||||
profilers = query_options.profilers if query_options is not None else None
|
||||
q = Query(query=query, dialect=dialect, extern=_BaseQueryApi._build_flux_ast(params, profilers))
|
||||
|
||||
if profilers:
|
||||
print("\n===============")
|
||||
print("Profiler: query")
|
||||
print("===============")
|
||||
print(query)
|
||||
|
||||
if kwargs.get('dataframe_query', False):
|
||||
MissingPivotFunction.print_warning(query)
|
||||
|
||||
return q
|
||||
|
||||
@staticmethod
|
||||
def _params_to_extern_ast(params: dict) -> List['OptionStatement']:
|
||||
|
||||
statements = []
|
||||
for key, value in params.items():
|
||||
expression = _BaseQueryApi._parm_to_extern_ast(value)
|
||||
if expression is None:
|
||||
continue
|
||||
|
||||
statements.append(OptionStatement("OptionStatement",
|
||||
VariableAssignment("VariableAssignment", Identifier("Identifier", key),
|
||||
expression)))
|
||||
return statements
|
||||
|
||||
@staticmethod
|
||||
def _parm_to_extern_ast(value) -> Union[Expression, None]:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, bool):
|
||||
return BooleanLiteral("BooleanLiteral", value)
|
||||
elif isinstance(value, int):
|
||||
return IntegerLiteral("IntegerLiteral", str(value))
|
||||
elif isinstance(value, float):
|
||||
return FloatLiteral("FloatLiteral", value)
|
||||
elif isinstance(value, datetime):
|
||||
value = get_date_helper().to_utc(value)
|
||||
nanoseconds = getattr(value, 'nanosecond', 0)
|
||||
fraction = f'{(value.microsecond * 1000 + nanoseconds):09d}'
|
||||
return DateTimeLiteral("DateTimeLiteral", value.strftime('%Y-%m-%dT%H:%M:%S.') + fraction + 'Z')
|
||||
elif isinstance(value, timedelta):
|
||||
_micro_delta = int(value / timedelta(microseconds=1))
|
||||
if _micro_delta < 0:
|
||||
return UnaryExpression("UnaryExpression", argument=DurationLiteral("DurationLiteral", [
|
||||
Duration(magnitude=-_micro_delta, unit="us")]), operator="-")
|
||||
else:
|
||||
return DurationLiteral("DurationLiteral", [Duration(magnitude=_micro_delta, unit="us")])
|
||||
elif isinstance(value, str):
|
||||
return StringLiteral("StringLiteral", str(value))
|
||||
elif isinstance(value, Iterable):
|
||||
return ArrayExpression("ArrayExpression",
|
||||
elements=list(map(lambda it: _BaseQueryApi._parm_to_extern_ast(it), value)))
|
||||
else:
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def _build_flux_ast(params: dict = None, profilers: List[str] = None):
|
||||
|
||||
imports = []
|
||||
body = []
|
||||
|
||||
if profilers is not None and len(profilers) > 0:
|
||||
imports.append(ImportDeclaration(
|
||||
"ImportDeclaration",
|
||||
path=StringLiteral("StringLiteral", "profiler")))
|
||||
|
||||
elements = []
|
||||
for profiler in profilers:
|
||||
elements.append(StringLiteral("StringLiteral", value=profiler))
|
||||
|
||||
member = MemberExpression(
|
||||
"MemberExpression",
|
||||
object=Identifier("Identifier", "profiler"),
|
||||
_property=Identifier("Identifier", "enabledProfilers"))
|
||||
|
||||
prof = OptionStatement(
|
||||
"OptionStatement",
|
||||
assignment=MemberAssignment(
|
||||
"MemberAssignment",
|
||||
member=member,
|
||||
init=ArrayExpression(
|
||||
"ArrayExpression",
|
||||
elements=elements)))
|
||||
|
||||
body.append(prof)
|
||||
|
||||
if params is not None:
|
||||
body.extend(_BaseQueryApi._params_to_extern_ast(params))
|
||||
|
||||
return File(package=None, name=None, type=None, imports=imports, body=body)
|
||||
|
||||
|
||||
class _BaseWriteApi(object):
|
||||
def __init__(self, influxdb_client, point_settings=None):
|
||||
self._influxdb_client = influxdb_client
|
||||
self._point_settings = point_settings
|
||||
self._write_service = WriteService(influxdb_client.api_client)
|
||||
if influxdb_client.default_tags:
|
||||
for key, value in influxdb_client.default_tags.items():
|
||||
self._point_settings.add_default_tag(key, value)
|
||||
|
||||
def _append_default_tag(self, key, val, record):
|
||||
from influxdb_client import Point
|
||||
if isinstance(record, bytes) or isinstance(record, str):
|
||||
pass
|
||||
elif isinstance(record, Point):
|
||||
record.tag(key, val)
|
||||
elif isinstance(record, dict):
|
||||
record.setdefault("tags", {})
|
||||
record.get("tags")[key] = val
|
||||
elif isinstance(record, Iterable):
|
||||
for item in record:
|
||||
self._append_default_tag(key, val, item)
|
||||
|
||||
def _append_default_tags(self, record):
|
||||
if self._point_settings.defaultTags and record is not None:
|
||||
for key, val in self._point_settings.defaultTags.items():
|
||||
self._append_default_tag(key, val, record)
|
||||
|
||||
def _serialize(self, record, write_precision, payload, **kwargs):
|
||||
from influxdb_client import Point
|
||||
if isinstance(record, bytes):
|
||||
payload[write_precision].append(record)
|
||||
|
||||
elif isinstance(record, str):
|
||||
self._serialize(record.encode(_UTF_8_encoding), write_precision, payload, **kwargs)
|
||||
|
||||
elif isinstance(record, Point):
|
||||
precision_from_point = kwargs.get('precision_from_point', True)
|
||||
precision = record.write_precision if precision_from_point else write_precision
|
||||
self._serialize(record.to_line_protocol(precision=precision), precision, payload, **kwargs)
|
||||
|
||||
elif isinstance(record, dict):
|
||||
self._serialize(Point.from_dict(record, write_precision=write_precision, **kwargs),
|
||||
write_precision, payload, **kwargs)
|
||||
elif 'DataFrame' in type(record).__name__:
|
||||
serializer = DataframeSerializer(record, self._point_settings, write_precision, **kwargs)
|
||||
self._serialize(serializer.serialize(), write_precision, payload, **kwargs)
|
||||
elif hasattr(record, "_asdict"):
|
||||
# noinspection PyProtectedMember
|
||||
self._serialize(record._asdict(), write_precision, payload, **kwargs)
|
||||
elif _HAS_DATACLASS and dataclasses.is_dataclass(record):
|
||||
self._serialize(dataclasses.asdict(record), write_precision, payload, **kwargs)
|
||||
elif isinstance(record, Iterable):
|
||||
for item in record:
|
||||
self._serialize(item, write_precision, payload, **kwargs)
|
||||
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
class _BaseDeleteApi(object):
|
||||
def __init__(self, influxdb_client):
|
||||
self._influxdb_client = influxdb_client
|
||||
self._service = DeleteService(influxdb_client.api_client)
|
||||
|
||||
def _prepare_predicate_request(self, start, stop, predicate):
|
||||
date_helper = get_date_helper()
|
||||
if isinstance(start, datetime):
|
||||
start = date_helper.to_utc(start)
|
||||
if isinstance(stop, datetime):
|
||||
stop = date_helper.to_utc(stop)
|
||||
predicate_request = DeletePredicateRequest(start=start, stop=stop, predicate=predicate)
|
||||
return predicate_request
|
||||
|
||||
|
||||
class _Configuration(Configuration):
|
||||
def __init__(self):
|
||||
Configuration.__init__(self)
|
||||
self.enable_gzip = False
|
||||
self.username = None
|
||||
self.password = None
|
||||
|
||||
def update_request_header_params(self, path: str, params: dict):
|
||||
super().update_request_header_params(path, params)
|
||||
if self.enable_gzip:
|
||||
# GZIP Request
|
||||
if path == '/api/v2/write':
|
||||
params["Content-Encoding"] = "gzip"
|
||||
params["Accept-Encoding"] = "identity"
|
||||
pass
|
||||
# GZIP Response
|
||||
if path == '/api/v2/query':
|
||||
# params["Content-Encoding"] = "gzip"
|
||||
params["Accept-Encoding"] = "gzip"
|
||||
pass
|
||||
pass
|
||||
pass
|
||||
|
||||
def update_request_body(self, path: str, body):
|
||||
_body = super().update_request_body(path, body)
|
||||
if self.enable_gzip:
|
||||
# GZIP Request
|
||||
if path == '/api/v2/write':
|
||||
import gzip
|
||||
if isinstance(_body, bytes):
|
||||
return gzip.compress(data=_body)
|
||||
else:
|
||||
return gzip.compress(bytes(_body, _UTF_8_encoding))
|
||||
|
||||
return _body
|
||||
|
||||
|
||||
def _to_bool(bool_value):
|
||||
return str(bool_value).lower() in ("yes", "true")
|
||||
|
||||
|
||||
def _to_int(int_value):
|
||||
return int(int_value) if int_value is not None else None
|
||||
@@ -0,0 +1,66 @@
|
||||
|
||||
|
||||
class _Page:
|
||||
def __init__(self, values, has_next, next_after):
|
||||
self.has_next = has_next
|
||||
self.values = values
|
||||
self.next_after = next_after
|
||||
|
||||
@staticmethod
|
||||
def empty():
|
||||
return _Page([], False, None)
|
||||
|
||||
@staticmethod
|
||||
def initial(after):
|
||||
return _Page([], True, after)
|
||||
|
||||
|
||||
class _PageIterator:
|
||||
def __init__(self, page: _Page, get_next_page):
|
||||
self.page = page
|
||||
self.get_next_page = get_next_page
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
if not self.page.values:
|
||||
if self.page.has_next:
|
||||
self.page = self.get_next_page(self.page)
|
||||
if not self.page.values:
|
||||
raise StopIteration
|
||||
return self.page.values.pop(0)
|
||||
|
||||
|
||||
class _Paginated:
|
||||
def __init__(self, paginated_getter, pluck_page_resources_from_response):
|
||||
self.paginated_getter = paginated_getter
|
||||
self.pluck_page_resources_from_response = pluck_page_resources_from_response
|
||||
|
||||
def find_iter(self, **kwargs):
|
||||
"""Iterate over resources with pagination.
|
||||
|
||||
:key str org: The organization name.
|
||||
:key str org_id: The organization ID.
|
||||
:key str after: The last resource ID from which to seek from (but not including).
|
||||
:key int limit: the maximum number of items per page
|
||||
:return: resources iterator
|
||||
"""
|
||||
|
||||
def get_next_page(page: _Page):
|
||||
return self._find_next_page(page, **kwargs)
|
||||
|
||||
return iter(_PageIterator(_Page.initial(kwargs.get('after')), get_next_page))
|
||||
|
||||
def _find_next_page(self, page: _Page, **kwargs):
|
||||
if not page.has_next:
|
||||
return _Page.empty()
|
||||
|
||||
kw_args = {**kwargs, 'after': page.next_after} if page.next_after is not None else kwargs
|
||||
response = self.paginated_getter(**kw_args)
|
||||
|
||||
resources = self.pluck_page_resources_from_response(response)
|
||||
has_next = response.links.next is not None
|
||||
last_id = resources[-1].id if resources else None
|
||||
|
||||
return _Page(resources, has_next, last_id)
|
||||
@@ -0,0 +1,136 @@
|
||||
"""Authorization is about managing the security of your InfluxDB instance."""
|
||||
|
||||
from influxdb_client import Authorization, AuthorizationsService, User, Organization
|
||||
|
||||
|
||||
class AuthorizationsApi(object):
|
||||
"""Implementation for '/api/v2/authorizations' endpoint."""
|
||||
|
||||
def __init__(self, influxdb_client):
|
||||
"""Initialize defaults."""
|
||||
self._influxdb_client = influxdb_client
|
||||
self._authorizations_service = AuthorizationsService(influxdb_client.api_client)
|
||||
|
||||
def create_authorization(self, org_id: str = None, permissions: list = None,
|
||||
authorization: Authorization = None) -> Authorization:
|
||||
"""
|
||||
Create an authorization.
|
||||
|
||||
:type permissions: list of Permission
|
||||
:param org_id: organization id
|
||||
:param permissions: list of permissions
|
||||
:type authorization: authorization object
|
||||
|
||||
"""
|
||||
if authorization is not None:
|
||||
if not isinstance(authorization, Authorization):
|
||||
raise TypeError(f"Attempt to use non-Authorization value for authorization: {authorization}")
|
||||
return self._authorizations_service.post_authorizations(authorization_post_request=authorization)
|
||||
|
||||
# if org_id is not None and permissions is not None:
|
||||
authorization = Authorization(org_id=org_id, permissions=permissions)
|
||||
return self._authorizations_service.post_authorizations(authorization_post_request=authorization)
|
||||
|
||||
def find_authorization_by_id(self, auth_id: str) -> Authorization:
|
||||
"""
|
||||
Find authorization by id.
|
||||
|
||||
:param auth_id: authorization id
|
||||
:return: Authorization
|
||||
"""
|
||||
return self._authorizations_service.get_authorizations_id(auth_id=auth_id)
|
||||
|
||||
def find_authorizations(self, **kwargs):
|
||||
"""
|
||||
Get a list of all authorizations.
|
||||
|
||||
:key str user_id: filter authorizations belonging to a user id
|
||||
:key str user: filter authorizations belonging to a user name
|
||||
:key str org_id: filter authorizations belonging to a org id
|
||||
:key str org: filter authorizations belonging to a org name
|
||||
:return: Authorizations
|
||||
"""
|
||||
authorizations = self._authorizations_service.get_authorizations(**kwargs)
|
||||
|
||||
return authorizations.authorizations
|
||||
|
||||
def find_authorizations_by_user(self, user: User):
|
||||
"""
|
||||
Find authorization by User.
|
||||
|
||||
:return: Authorization list
|
||||
"""
|
||||
return self.find_authorizations(user_id=user.id)
|
||||
|
||||
def find_authorizations_by_user_id(self, user_id: str):
|
||||
"""
|
||||
Find authorization by user id.
|
||||
|
||||
:return: Authorization list
|
||||
"""
|
||||
return self.find_authorizations(user_id=user_id)
|
||||
|
||||
def find_authorizations_by_user_name(self, user_name: str):
|
||||
"""
|
||||
Find authorization by user name.
|
||||
|
||||
:return: Authorization list
|
||||
"""
|
||||
return self.find_authorizations(user=user_name)
|
||||
|
||||
def find_authorizations_by_org(self, org: Organization):
|
||||
"""
|
||||
Find authorization by user name.
|
||||
|
||||
:return: Authorization list
|
||||
"""
|
||||
if isinstance(org, Organization):
|
||||
return self.find_authorizations(org_id=org.id)
|
||||
|
||||
def find_authorizations_by_org_name(self, org_name: str):
|
||||
"""
|
||||
Find authorization by org name.
|
||||
|
||||
:return: Authorization list
|
||||
"""
|
||||
return self.find_authorizations(org=org_name)
|
||||
|
||||
def find_authorizations_by_org_id(self, org_id: str):
|
||||
"""
|
||||
Find authorization by org id.
|
||||
|
||||
:return: Authorization list
|
||||
"""
|
||||
return self.find_authorizations(org_id=org_id)
|
||||
|
||||
def update_authorization(self, auth):
|
||||
"""
|
||||
Update authorization object.
|
||||
|
||||
:param auth:
|
||||
:return:
|
||||
"""
|
||||
return self._authorizations_service.patch_authorizations_id(auth_id=auth.id, authorization_update_request=auth)
|
||||
|
||||
def clone_authorization(self, auth) -> Authorization:
|
||||
"""Clone an authorization."""
|
||||
if isinstance(auth, Authorization):
|
||||
cloned = Authorization(org_id=auth.org_id, permissions=auth.permissions)
|
||||
# cloned.description = auth.description
|
||||
# cloned.status = auth.status
|
||||
return self.create_authorization(authorization=cloned)
|
||||
|
||||
if isinstance(auth, str):
|
||||
authorization = self.find_authorization_by_id(auth)
|
||||
return self.clone_authorization(auth=authorization)
|
||||
|
||||
raise ValueError("Invalid argument")
|
||||
|
||||
def delete_authorization(self, auth):
|
||||
"""Delete a authorization."""
|
||||
if isinstance(auth, Authorization):
|
||||
return self._authorizations_service.delete_authorizations_id(auth_id=auth.id)
|
||||
|
||||
if isinstance(auth, str):
|
||||
return self._authorizations_service.delete_authorizations_id(auth_id=auth)
|
||||
raise ValueError("Invalid argument")
|
||||
@@ -0,0 +1,132 @@
|
||||
"""
|
||||
A bucket is a named location where time series data is stored.
|
||||
|
||||
All buckets have a retention policy, a duration of time that each data point persists.
|
||||
A bucket belongs to an organization.
|
||||
"""
|
||||
import warnings
|
||||
|
||||
from influxdb_client import BucketsService, Bucket, PostBucketRequest, PatchBucketRequest
|
||||
from influxdb_client.client.util.helpers import get_org_query_param
|
||||
from influxdb_client.client._pages import _Paginated
|
||||
|
||||
|
||||
class BucketsApi(object):
|
||||
"""Implementation for '/api/v2/buckets' endpoint."""
|
||||
|
||||
def __init__(self, influxdb_client):
|
||||
"""Initialize defaults."""
|
||||
self._influxdb_client = influxdb_client
|
||||
self._buckets_service = BucketsService(influxdb_client.api_client)
|
||||
|
||||
def create_bucket(self, bucket=None, bucket_name=None, org_id=None, retention_rules=None,
|
||||
description=None, org=None) -> Bucket:
|
||||
"""Create a bucket.
|
||||
|
||||
:param Bucket|PostBucketRequest bucket: bucket to create
|
||||
:param bucket_name: bucket name
|
||||
:param description: bucket description
|
||||
:param org_id: org_id
|
||||
:param bucket_name: bucket name
|
||||
:param retention_rules: retention rules array or single BucketRetentionRules
|
||||
:param str, Organization org: specifies the organization for create the bucket;
|
||||
Take the ``ID``, ``Name`` or ``Organization``.
|
||||
If not specified the default value from ``InfluxDBClient.org`` is used.
|
||||
:return: Bucket
|
||||
If the method is called asynchronously,
|
||||
returns the request thread.
|
||||
"""
|
||||
if retention_rules is None:
|
||||
retention_rules = []
|
||||
|
||||
rules = []
|
||||
|
||||
if isinstance(retention_rules, list):
|
||||
rules.extend(retention_rules)
|
||||
else:
|
||||
rules.append(retention_rules)
|
||||
|
||||
if org_id is not None:
|
||||
warnings.warn("org_id is deprecated; use org", DeprecationWarning)
|
||||
|
||||
if bucket is None:
|
||||
bucket = PostBucketRequest(name=bucket_name,
|
||||
retention_rules=rules,
|
||||
description=description,
|
||||
org_id=get_org_query_param(org=(org_id if org is None else org),
|
||||
client=self._influxdb_client,
|
||||
required_id=True))
|
||||
|
||||
return self._buckets_service.post_buckets(post_bucket_request=bucket)
|
||||
|
||||
def update_bucket(self, bucket: Bucket) -> Bucket:
|
||||
"""Update a bucket.
|
||||
|
||||
:param bucket: Bucket update to apply (required)
|
||||
:return: Bucket
|
||||
"""
|
||||
request = PatchBucketRequest(name=bucket.name,
|
||||
description=bucket.description,
|
||||
retention_rules=bucket.retention_rules)
|
||||
|
||||
return self._buckets_service.patch_buckets_id(bucket_id=bucket.id, patch_bucket_request=request)
|
||||
|
||||
def delete_bucket(self, bucket):
|
||||
"""Delete a bucket.
|
||||
|
||||
:param bucket: bucket id or Bucket
|
||||
:return: Bucket
|
||||
"""
|
||||
if isinstance(bucket, Bucket):
|
||||
bucket_id = bucket.id
|
||||
else:
|
||||
bucket_id = bucket
|
||||
|
||||
return self._buckets_service.delete_buckets_id(bucket_id=bucket_id)
|
||||
|
||||
def find_bucket_by_id(self, id):
|
||||
"""Find bucket by ID.
|
||||
|
||||
:param id:
|
||||
:return:
|
||||
"""
|
||||
return self._buckets_service.get_buckets_id(id)
|
||||
|
||||
def find_bucket_by_name(self, bucket_name):
|
||||
"""Find bucket by name.
|
||||
|
||||
:param bucket_name: bucket name
|
||||
:return: Bucket
|
||||
"""
|
||||
buckets = self._buckets_service.get_buckets(name=bucket_name)
|
||||
|
||||
if len(buckets.buckets) > 0:
|
||||
return buckets.buckets[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
def find_buckets(self, **kwargs):
|
||||
"""List buckets.
|
||||
|
||||
:key int offset: Offset for pagination
|
||||
:key int limit: Limit for pagination
|
||||
:key str after: The last resource ID from which to seek from (but not including).
|
||||
This is to be used instead of `offset`.
|
||||
:key str org: The organization name.
|
||||
:key str org_id: The organization ID.
|
||||
:key str name: Only returns buckets with a specific name.
|
||||
:return: Buckets
|
||||
"""
|
||||
return self._buckets_service.get_buckets(**kwargs)
|
||||
|
||||
def find_buckets_iter(self, **kwargs):
|
||||
"""Iterate over all buckets with pagination.
|
||||
|
||||
:key str name: Only returns buckets with the specified name
|
||||
:key str org: The organization name.
|
||||
:key str org_id: The organization ID.
|
||||
:key str after: The last resource ID from which to seek from (but not including).
|
||||
:key int limit: the maximum number of buckets in one page
|
||||
:return: Buckets iterator
|
||||
"""
|
||||
return _Paginated(self._buckets_service.get_buckets, lambda response: response.buckets).find_iter(**kwargs)
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Delete time series data from InfluxDB."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Union
|
||||
|
||||
from influxdb_client import Organization
|
||||
from influxdb_client.client._base import _BaseDeleteApi
|
||||
from influxdb_client.client.util.helpers import get_org_query_param
|
||||
|
||||
|
||||
class DeleteApi(_BaseDeleteApi):
|
||||
"""Implementation for '/api/v2/delete' endpoint."""
|
||||
|
||||
def __init__(self, influxdb_client):
|
||||
"""Initialize defaults."""
|
||||
super().__init__(influxdb_client)
|
||||
|
||||
def delete(self, start: Union[str, datetime], stop: Union[str, datetime], predicate: str, bucket: str,
|
||||
org: Union[str, Organization, None] = None) -> None:
|
||||
"""
|
||||
Delete Time series data from InfluxDB.
|
||||
|
||||
:param str, datetime.datetime start: start time
|
||||
:param str, datetime.datetime stop: stop time
|
||||
:param str predicate: predicate
|
||||
:param str bucket: bucket id or name from which data will be deleted
|
||||
:param str, Organization org: specifies the organization to delete data from.
|
||||
Take the ``ID``, ``Name`` or ``Organization``.
|
||||
If not specified the default value from ``InfluxDBClient.org`` is used.
|
||||
:return:
|
||||
"""
|
||||
predicate_request = self._prepare_predicate_request(start, stop, predicate)
|
||||
org_param = get_org_query_param(org=org, client=self._influxdb_client, required_id=False)
|
||||
|
||||
return self._service.post_delete(delete_predicate_request=predicate_request, bucket=bucket, org=org_param)
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Delete time series data from InfluxDB."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Union
|
||||
|
||||
from influxdb_client import Organization
|
||||
from influxdb_client.client._base import _BaseDeleteApi
|
||||
from influxdb_client.client.util.helpers import get_org_query_param
|
||||
|
||||
|
||||
class DeleteApiAsync(_BaseDeleteApi):
|
||||
"""Async implementation for '/api/v2/delete' endpoint."""
|
||||
|
||||
def __init__(self, influxdb_client):
|
||||
"""Initialize defaults."""
|
||||
super().__init__(influxdb_client)
|
||||
|
||||
async def delete(self, start: Union[str, datetime], stop: Union[str, datetime], predicate: str, bucket: str,
|
||||
org: Union[str, Organization, None] = None) -> bool:
|
||||
"""
|
||||
Delete Time series data from InfluxDB.
|
||||
|
||||
:param str, datetime.datetime start: start time
|
||||
:param str, datetime.datetime stop: stop time
|
||||
:param str predicate: predicate
|
||||
:param str bucket: bucket id or name from which data will be deleted
|
||||
:param str, Organization org: specifies the organization to delete data from.
|
||||
Take the ``ID``, ``Name`` or ``Organization``.
|
||||
If not specified the default value from ``InfluxDBClientAsync.org`` is used.
|
||||
:return: ``True`` for successfully deleted data, otherwise raise an exception
|
||||
"""
|
||||
predicate_request = self._prepare_predicate_request(start, stop, predicate)
|
||||
org_param = get_org_query_param(org=org, client=self._influxdb_client, required_id=False)
|
||||
|
||||
response = await self._service.post_delete_async(delete_predicate_request=predicate_request, bucket=bucket,
|
||||
org=org_param, _return_http_data_only=False)
|
||||
return response[1] == 204
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Exceptions utils for InfluxDB."""
|
||||
|
||||
import logging
|
||||
|
||||
from urllib3 import HTTPResponse
|
||||
|
||||
logger = logging.getLogger('influxdb_client.client.exceptions')
|
||||
|
||||
|
||||
class InfluxDBError(Exception):
|
||||
"""Raised when a server error occurs."""
|
||||
|
||||
def __init__(self, response: HTTPResponse = None, message: str = None):
|
||||
"""Initialize the InfluxDBError handler."""
|
||||
if response is not None:
|
||||
self.response = response
|
||||
self.message = self._get_message(response)
|
||||
if isinstance(response, HTTPResponse): # response is HTTPResponse
|
||||
self.headers = response.headers
|
||||
self.retry_after = response.headers.get('Retry-After')
|
||||
else: # response is RESTResponse
|
||||
self.headers = response.getheaders()
|
||||
self.retry_after = response.getheader('Retry-After')
|
||||
else:
|
||||
self.response = None
|
||||
self.message = message or 'no response'
|
||||
self.retry_after = None
|
||||
super().__init__(self.message)
|
||||
|
||||
def _get_message(self, response):
|
||||
# Body
|
||||
if response.data:
|
||||
import json
|
||||
try:
|
||||
return json.loads(response.data)["message"]
|
||||
except Exception as e:
|
||||
logging.debug(f"Cannot parse error response to JSON: {response.data}, {e}")
|
||||
return response.data
|
||||
|
||||
# Header
|
||||
for header_key in ["X-Platform-Error-Code", "X-Influx-Error", "X-InfluxDb-Error"]:
|
||||
header_value = response.getheader(header_key)
|
||||
if header_value is not None:
|
||||
return header_value
|
||||
|
||||
# Http Status
|
||||
return response.reason
|
||||
@@ -0,0 +1,408 @@
|
||||
"""Parsing response from InfluxDB to FluxStructures or DataFrame."""
|
||||
|
||||
import base64
|
||||
import codecs
|
||||
import csv as csv_parser
|
||||
import warnings
|
||||
from enum import Enum
|
||||
from typing import List
|
||||
|
||||
from influxdb_client.client.flux_table import FluxTable, FluxColumn, FluxRecord, TableList
|
||||
from influxdb_client.client.util.date_utils import get_date_helper
|
||||
from influxdb_client.rest import _UTF_8_encoding
|
||||
|
||||
ANNOTATION_DEFAULT = "#default"
|
||||
ANNOTATION_GROUP = "#group"
|
||||
ANNOTATION_DATATYPE = "#datatype"
|
||||
ANNOTATIONS = [ANNOTATION_DEFAULT, ANNOTATION_GROUP, ANNOTATION_DATATYPE]
|
||||
|
||||
|
||||
class FluxQueryException(Exception):
|
||||
"""The exception from InfluxDB."""
|
||||
|
||||
def __init__(self, message, reference) -> None:
|
||||
"""Initialize defaults."""
|
||||
self.message = message
|
||||
self.reference = reference
|
||||
|
||||
|
||||
class FluxCsvParserException(Exception):
|
||||
"""The exception for not parsable data."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class FluxSerializationMode(Enum):
|
||||
"""The type how we want to serialize data."""
|
||||
|
||||
tables = 1
|
||||
stream = 2
|
||||
dataFrame = 3
|
||||
|
||||
|
||||
class FluxResponseMetadataMode(Enum):
|
||||
"""The configuration for expected amount of metadata response from InfluxDB."""
|
||||
|
||||
full = 1
|
||||
# useful for Invokable scripts
|
||||
only_names = 2
|
||||
|
||||
|
||||
class _FluxCsvParserMetadata(object):
|
||||
def __init__(self):
|
||||
self.table_index = 0
|
||||
self.table_id = -1
|
||||
self.start_new_table = False
|
||||
self.table = None
|
||||
self.groups = []
|
||||
self.parsing_state_error = False
|
||||
|
||||
|
||||
class FluxCsvParser(object):
|
||||
"""Parse to processing response from InfluxDB to FluxStructures or DataFrame."""
|
||||
|
||||
def __init__(self, response, serialization_mode: FluxSerializationMode,
|
||||
data_frame_index: List[str] = None, query_options=None,
|
||||
response_metadata_mode: FluxResponseMetadataMode = FluxResponseMetadataMode.full,
|
||||
use_extension_dtypes=False) -> None:
|
||||
"""
|
||||
Initialize defaults.
|
||||
|
||||
:param response: HTTP response from a HTTP client.
|
||||
Acceptable types: `urllib3.response.HTTPResponse`, `aiohttp.client_reqrep.ClientResponse`.
|
||||
"""
|
||||
self._response = response
|
||||
self.tables = TableList()
|
||||
self._serialization_mode = serialization_mode
|
||||
self._response_metadata_mode = response_metadata_mode
|
||||
self._use_extension_dtypes = use_extension_dtypes
|
||||
self._data_frame_index = data_frame_index
|
||||
self._data_frame_values = []
|
||||
self._profilers = query_options.profilers if query_options is not None else None
|
||||
self._profiler_callback = query_options.profiler_callback if query_options is not None else None
|
||||
self._async_mode = True if 'ClientResponse' in type(response).__name__ else False
|
||||
|
||||
def _close(self):
|
||||
self._response.close()
|
||||
|
||||
def __enter__(self):
|
||||
"""Initialize CSV reader."""
|
||||
# response can be exhausted by logger, so we have to use data that has already been read
|
||||
if hasattr(self._response, 'closed') and self._response.closed:
|
||||
from io import StringIO
|
||||
self._reader = csv_parser.reader(StringIO(self._response.data.decode(_UTF_8_encoding)))
|
||||
else:
|
||||
self._reader = csv_parser.reader(codecs.iterdecode(self._response, _UTF_8_encoding))
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Close HTTP response."""
|
||||
self._close()
|
||||
|
||||
async def __aenter__(self) -> 'FluxCsvParser':
|
||||
"""Initialize CSV reader."""
|
||||
from aiocsv import AsyncReader
|
||||
self._reader = AsyncReader(_StreamReaderToWithAsyncRead(self._response.content))
|
||||
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
||||
"""Shutdown the client."""
|
||||
self.__exit__(exc_type, exc_val, exc_tb)
|
||||
|
||||
def generator(self):
|
||||
"""Return Python generator."""
|
||||
with self as parser:
|
||||
for val in parser._parse_flux_response():
|
||||
yield val
|
||||
|
||||
def generator_async(self):
|
||||
"""Return Python async-generator."""
|
||||
return self._parse_flux_response_async()
|
||||
|
||||
def _parse_flux_response(self):
|
||||
metadata = _FluxCsvParserMetadata()
|
||||
|
||||
for csv in self._reader:
|
||||
for val in self._parse_flux_response_row(metadata, csv):
|
||||
yield val
|
||||
|
||||
# Return latest DataFrame
|
||||
if (self._serialization_mode is FluxSerializationMode.dataFrame) & hasattr(self, '_data_frame'):
|
||||
df = self._prepare_data_frame()
|
||||
if not self._is_profiler_table(metadata.table):
|
||||
yield df
|
||||
|
||||
async def _parse_flux_response_async(self):
|
||||
metadata = _FluxCsvParserMetadata()
|
||||
|
||||
try:
|
||||
async for csv in self._reader:
|
||||
for val in self._parse_flux_response_row(metadata, csv):
|
||||
yield val
|
||||
|
||||
# Return latest DataFrame
|
||||
if (self._serialization_mode is FluxSerializationMode.dataFrame) & hasattr(self, '_data_frame'):
|
||||
df = self._prepare_data_frame()
|
||||
if not self._is_profiler_table(metadata.table):
|
||||
yield df
|
||||
except BaseException as e:
|
||||
e_type = type(e).__name__
|
||||
if "CancelledError" in e_type or "TimeoutError" in e_type:
|
||||
e.add_note("Stream cancelled during read. Recommended: Check Influxdb client `timeout` setting.")
|
||||
raise
|
||||
finally:
|
||||
self._close()
|
||||
|
||||
def _parse_flux_response_row(self, metadata, csv):
|
||||
if len(csv) < 1:
|
||||
# Skip empty line in results (new line is used as a delimiter between tables or table and error)
|
||||
pass
|
||||
|
||||
elif "error" == csv[1] and "reference" == csv[2]:
|
||||
metadata.parsing_state_error = True
|
||||
|
||||
else:
|
||||
# Throw InfluxException with error response
|
||||
if metadata.parsing_state_error:
|
||||
error = csv[1]
|
||||
reference_value = csv[2]
|
||||
raise FluxQueryException(error, reference_value)
|
||||
|
||||
token = csv[0]
|
||||
# start new table
|
||||
if (token in ANNOTATIONS and not metadata.start_new_table) or \
|
||||
(self._response_metadata_mode is FluxResponseMetadataMode.only_names and not metadata.table):
|
||||
|
||||
# Return already parsed DataFrame
|
||||
if (self._serialization_mode is FluxSerializationMode.dataFrame) & hasattr(self, '_data_frame'):
|
||||
df = self._prepare_data_frame()
|
||||
if not self._is_profiler_table(metadata.table):
|
||||
yield df
|
||||
|
||||
metadata.start_new_table = True
|
||||
metadata.table = FluxTable()
|
||||
self._insert_table(metadata.table, metadata.table_index)
|
||||
metadata.table_index = metadata.table_index + 1
|
||||
metadata.table_id = -1
|
||||
elif metadata.table is None:
|
||||
raise FluxCsvParserException("Unable to parse CSV response. FluxTable definition was not found.")
|
||||
|
||||
# # datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,double,string,string,string
|
||||
if ANNOTATION_DATATYPE == token:
|
||||
self.add_data_types(metadata.table, csv)
|
||||
|
||||
elif ANNOTATION_GROUP == token:
|
||||
metadata.groups = csv
|
||||
|
||||
elif ANNOTATION_DEFAULT == token:
|
||||
self.add_default_empty_values(metadata.table, csv)
|
||||
|
||||
else:
|
||||
# parse column names
|
||||
if metadata.start_new_table:
|
||||
# Invokable scripts doesn't supports dialect => all columns are string
|
||||
if not metadata.table.columns and \
|
||||
self._response_metadata_mode is FluxResponseMetadataMode.only_names:
|
||||
self.add_data_types(metadata.table, list(map(lambda column: 'string', csv)))
|
||||
metadata.groups = list(map(lambda column: 'false', csv))
|
||||
self.add_groups(metadata.table, metadata.groups)
|
||||
self.add_column_names_and_tags(metadata.table, csv)
|
||||
metadata.start_new_table = False
|
||||
# Create DataFrame with default values
|
||||
if self._serialization_mode is FluxSerializationMode.dataFrame:
|
||||
from ..extras import pd
|
||||
labels = list(map(lambda it: it.label, metadata.table.columns))
|
||||
self._data_frame = pd.DataFrame(data=[], columns=labels, index=None)
|
||||
pass
|
||||
else:
|
||||
|
||||
# to int conversions todo
|
||||
current_id = int(csv[2])
|
||||
if metadata.table_id == -1:
|
||||
metadata.table_id = current_id
|
||||
|
||||
if metadata.table_id != current_id:
|
||||
# create new table with previous column headers settings
|
||||
flux_columns = metadata.table.columns
|
||||
metadata.table = FluxTable()
|
||||
metadata.table.columns.extend(flux_columns)
|
||||
self._insert_table(metadata.table, metadata.table_index)
|
||||
metadata.table_index = metadata.table_index + 1
|
||||
metadata.table_id = current_id
|
||||
|
||||
flux_record = self.parse_record(metadata.table_index - 1, metadata.table, csv)
|
||||
|
||||
if self._is_profiler_record(flux_record):
|
||||
self._print_profiler_info(flux_record)
|
||||
else:
|
||||
if self._serialization_mode is FluxSerializationMode.tables:
|
||||
self.tables[metadata.table_index - 1].records.append(flux_record)
|
||||
|
||||
if self._serialization_mode is FluxSerializationMode.stream:
|
||||
yield flux_record
|
||||
|
||||
if self._serialization_mode is FluxSerializationMode.dataFrame:
|
||||
self._data_frame_values.append(flux_record.values)
|
||||
pass
|
||||
|
||||
def _prepare_data_frame(self):
|
||||
from ..extras import pd
|
||||
|
||||
# We have to create temporary DataFrame because we want to preserve default column values
|
||||
_temp_df = pd.DataFrame(self._data_frame_values)
|
||||
self._data_frame_values = []
|
||||
|
||||
# Custom DataFrame index
|
||||
if self._data_frame_index:
|
||||
self._data_frame = self._data_frame.set_index(self._data_frame_index)
|
||||
_temp_df = _temp_df.set_index(self._data_frame_index)
|
||||
|
||||
# Append data
|
||||
df = pd.concat([self._data_frame.astype(_temp_df.dtypes), _temp_df])
|
||||
|
||||
if self._use_extension_dtypes:
|
||||
return df.convert_dtypes()
|
||||
return df
|
||||
|
||||
def parse_record(self, table_index, table, csv):
|
||||
"""Parse one record."""
|
||||
record = FluxRecord(table_index)
|
||||
|
||||
for fluxColumn in table.columns:
|
||||
column_name = fluxColumn.label
|
||||
str_val = csv[fluxColumn.index + 1]
|
||||
record.values[column_name] = self._to_value(str_val, fluxColumn)
|
||||
record.row.append(record.values[column_name])
|
||||
|
||||
return record
|
||||
|
||||
def _to_value(self, str_val, column):
|
||||
|
||||
if str_val == '' or str_val is None:
|
||||
default_value = column.default_value
|
||||
if default_value == '' or default_value is None:
|
||||
if self._serialization_mode is FluxSerializationMode.dataFrame:
|
||||
if self._use_extension_dtypes:
|
||||
from ..extras import pd
|
||||
return pd.NA
|
||||
return None
|
||||
return None
|
||||
return self._to_value(default_value, column)
|
||||
|
||||
if "string" == column.data_type:
|
||||
return str_val
|
||||
|
||||
if "boolean" == column.data_type:
|
||||
return "true" == str_val
|
||||
|
||||
if "unsignedLong" == column.data_type or "long" == column.data_type:
|
||||
return int(str_val)
|
||||
|
||||
if "double" == column.data_type:
|
||||
return float(str_val)
|
||||
|
||||
if "base64Binary" == column.data_type:
|
||||
return base64.b64decode(str_val)
|
||||
|
||||
if "dateTime:RFC3339" == column.data_type or "dateTime:RFC3339Nano" == column.data_type:
|
||||
return get_date_helper().parse_date(str_val)
|
||||
|
||||
if "duration" == column.data_type:
|
||||
# todo better type ?
|
||||
return int(str_val)
|
||||
|
||||
@staticmethod
|
||||
def add_data_types(table, data_types):
|
||||
"""Add data types to columns."""
|
||||
for index in range(1, len(data_types)):
|
||||
column_def = FluxColumn(index=index - 1, data_type=data_types[index])
|
||||
table.columns.append(column_def)
|
||||
|
||||
@staticmethod
|
||||
def add_groups(table, csv):
|
||||
"""Add group keys to columns."""
|
||||
i = 1
|
||||
for column in table.columns:
|
||||
column.group = csv[i] == "true"
|
||||
i += 1
|
||||
|
||||
@staticmethod
|
||||
def add_default_empty_values(table, default_values):
|
||||
"""Add default values to columns."""
|
||||
i = 1
|
||||
for column in table.columns:
|
||||
column.default_value = default_values[i]
|
||||
i += 1
|
||||
|
||||
@staticmethod
|
||||
def add_column_names_and_tags(table, csv):
|
||||
"""Add labels to columns."""
|
||||
if len(csv) != len(set(csv)):
|
||||
message = f"""The response contains columns with duplicated names: '{csv}'.
|
||||
|
||||
You should use the 'record.row' to access your data instead of 'record.values' dictionary.
|
||||
"""
|
||||
warnings.warn(message, UserWarning)
|
||||
print(message)
|
||||
i = 1
|
||||
for column in table.columns:
|
||||
column.label = csv[i]
|
||||
i += 1
|
||||
|
||||
def _insert_table(self, table, table_index):
|
||||
if self._serialization_mode is FluxSerializationMode.tables:
|
||||
self.tables.insert(table_index, table)
|
||||
|
||||
def _is_profiler_record(self, flux_record: FluxRecord) -> bool:
|
||||
if not self._profilers:
|
||||
return False
|
||||
|
||||
for profiler in self._profilers:
|
||||
if "_measurement" in flux_record.values and flux_record["_measurement"] == "profiler/" + profiler:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _is_profiler_table(self, table: FluxTable) -> bool:
|
||||
|
||||
if not self._profilers:
|
||||
return False
|
||||
|
||||
return any(filter(lambda column: (column.default_value == "_profiler" and column.label == "result"),
|
||||
table.columns))
|
||||
|
||||
def table_list(self) -> TableList:
|
||||
"""Get the list of flux tables."""
|
||||
if not self._profilers:
|
||||
return self.tables
|
||||
else:
|
||||
return TableList(filter(lambda table: not self._is_profiler_table(table), self.tables))
|
||||
|
||||
def _print_profiler_info(self, flux_record: FluxRecord):
|
||||
if flux_record.get_measurement().startswith("profiler/"):
|
||||
if self._profiler_callback:
|
||||
self._profiler_callback(flux_record)
|
||||
else:
|
||||
msg = "Profiler: " + flux_record.get_measurement()
|
||||
print("\n" + len(msg) * "=")
|
||||
print(msg)
|
||||
print(len(msg) * "=")
|
||||
for name in flux_record.values:
|
||||
val = flux_record[name]
|
||||
if isinstance(val, str) and len(val) > 50:
|
||||
print(f"{name:<20}: \n\n{val}")
|
||||
elif val is not None:
|
||||
print(f"{name:<20}: {val:<20}")
|
||||
|
||||
|
||||
class _StreamReaderToWithAsyncRead:
|
||||
def __init__(self, response):
|
||||
self.response = response
|
||||
self.decoder = codecs.getincrementaldecoder(_UTF_8_encoding)()
|
||||
|
||||
async def read(self, size: int) -> str:
|
||||
raw_bytes = (await self.response.read(size))
|
||||
if not raw_bytes:
|
||||
return self.decoder.decode(b'', final=True)
|
||||
return self.decoder.decode(raw_bytes, final=False)
|
||||
@@ -0,0 +1,290 @@
|
||||
"""
|
||||
Flux employs a basic data model built from basic data types.
|
||||
|
||||
The data model consists of tables, records, columns.
|
||||
"""
|
||||
import codecs
|
||||
import csv
|
||||
from http.client import HTTPResponse
|
||||
from json import JSONEncoder
|
||||
from typing import List, Iterator
|
||||
from influxdb_client.rest import _UTF_8_encoding
|
||||
|
||||
|
||||
class FluxStructure:
|
||||
"""The data model consists of tables, records, columns."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class FluxStructureEncoder(JSONEncoder):
|
||||
"""The FluxStructure encoder to encode query results to JSON."""
|
||||
|
||||
def default(self, obj):
|
||||
"""Return serializable objects for JSONEncoder."""
|
||||
import datetime
|
||||
if isinstance(obj, FluxStructure):
|
||||
return obj.__dict__
|
||||
elif isinstance(obj, (datetime.datetime, datetime.date)):
|
||||
return obj.isoformat()
|
||||
return super().default(obj)
|
||||
|
||||
|
||||
class FluxTable(FluxStructure):
|
||||
"""
|
||||
A table is set of records with a common set of columns and a group key.
|
||||
|
||||
The table can be serialized into JSON by::
|
||||
|
||||
import json
|
||||
from influxdb_client.client.flux_table import FluxStructureEncoder
|
||||
|
||||
output = json.dumps(tables, cls=FluxStructureEncoder, indent=2)
|
||||
print(output)
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize defaults."""
|
||||
self.columns: List[FluxColumn] = []
|
||||
self.records: List[FluxRecord] = []
|
||||
|
||||
def get_group_key(self):
|
||||
"""
|
||||
Group key is a list of columns.
|
||||
|
||||
A table’s group key denotes which subset of the entire dataset is assigned to the table.
|
||||
"""
|
||||
return list(filter(lambda column: (column.group is True), self.columns))
|
||||
|
||||
def __str__(self):
|
||||
"""Return formatted output."""
|
||||
cls_name = type(self).__name__
|
||||
return cls_name + "() columns: " + str(len(self.columns)) + ", records: " + str(len(self.records))
|
||||
|
||||
def __repr__(self):
|
||||
"""Format for inspection."""
|
||||
return f"<{type(self).__name__}: {len(self.columns)} columns, {len(self.records)} records>"
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterate over records."""
|
||||
return iter(self.records)
|
||||
|
||||
|
||||
class FluxColumn(FluxStructure):
|
||||
"""A column has a label and a data type."""
|
||||
|
||||
def __init__(self, index=None, label=None, data_type=None, group=None, default_value=None) -> None:
|
||||
"""Initialize defaults."""
|
||||
self.default_value = default_value
|
||||
self.group = group
|
||||
self.data_type = data_type
|
||||
self.label = label
|
||||
self.index = index
|
||||
|
||||
def __repr__(self):
|
||||
"""Format for inspection."""
|
||||
fields = [repr(self.index)] + [
|
||||
f'{name}={getattr(self, name)!r}' for name in (
|
||||
'label', 'data_type', 'group', 'default_value'
|
||||
) if getattr(self, name) is not None
|
||||
]
|
||||
return f"{type(self).__name__}({', '.join(fields)})"
|
||||
|
||||
|
||||
class FluxRecord(FluxStructure):
|
||||
"""A record is a tuple of named values and is represented using an object type."""
|
||||
|
||||
def __init__(self, table, values=None) -> None:
|
||||
"""Initialize defaults."""
|
||||
if values is None:
|
||||
values = {}
|
||||
self.table = table
|
||||
self.values = values
|
||||
self.row = []
|
||||
|
||||
def get_start(self):
|
||||
"""Get '_start' value."""
|
||||
return self["_start"]
|
||||
|
||||
def get_stop(self):
|
||||
"""Get '_stop' value."""
|
||||
return self["_stop"]
|
||||
|
||||
def get_time(self):
|
||||
"""Get timestamp."""
|
||||
return self["_time"]
|
||||
|
||||
def get_value(self):
|
||||
"""Get field value."""
|
||||
return self["_value"]
|
||||
|
||||
def get_field(self):
|
||||
"""Get field name."""
|
||||
return self["_field"]
|
||||
|
||||
def get_measurement(self):
|
||||
"""Get measurement name."""
|
||||
return self["_measurement"]
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Get value by key."""
|
||||
return self.values.__getitem__(key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""Set value with key and value."""
|
||||
return self.values.__setitem__(key, value)
|
||||
|
||||
def __str__(self):
|
||||
"""Return formatted output."""
|
||||
cls_name = type(self).__name__
|
||||
return cls_name + "() table: " + str(self.table) + ", " + str(self.values)
|
||||
|
||||
def __repr__(self):
|
||||
"""Format for inspection."""
|
||||
return f"<{type(self).__name__}: field={self.values.get('_field')}, value={self.values.get('_value')}>"
|
||||
|
||||
|
||||
class TableList(List[FluxTable]):
|
||||
""":class:`~influxdb_client.client.flux_table.FluxTable` list with additionally functional to better handle of query result.""" # noqa: E501
|
||||
|
||||
def to_values(self, columns: List['str'] = None) -> List[List[object]]:
|
||||
"""
|
||||
Serialize query results to a flattened list of values.
|
||||
|
||||
:param columns: if not ``None`` then only specified columns are presented in results
|
||||
:return: :class:`~list` of values
|
||||
|
||||
Output example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
[
|
||||
['New York', datetime.datetime(2022, 6, 7, 11, 3, 22, 917593, tzinfo=tzutc()), 24.3],
|
||||
['Prague', datetime.datetime(2022, 6, 7, 11, 3, 22, 917593, tzinfo=tzutc()), 25.3],
|
||||
...
|
||||
]
|
||||
|
||||
Configure required columns:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from influxdb_client import InfluxDBClient
|
||||
|
||||
with InfluxDBClient(url="http://localhost:8086", token="my-token", org="my-org") as client:
|
||||
|
||||
# Query: using Table structure
|
||||
tables = client.query_api().query('from(bucket:"my-bucket") |> range(start: -10m)')
|
||||
|
||||
# Serialize to values
|
||||
output = tables.to_values(columns=['location', '_time', '_value'])
|
||||
print(output)
|
||||
"""
|
||||
|
||||
def filter_values(record):
|
||||
if columns is not None:
|
||||
return [record.values.get(k) for k in columns]
|
||||
return record.values.values()
|
||||
|
||||
return self._to_values(filter_values)
|
||||
|
||||
def to_json(self, columns: List['str'] = None, **kwargs) -> str:
|
||||
"""
|
||||
Serialize query results to a JSON formatted :class:`~str`.
|
||||
|
||||
:param columns: if not ``None`` then only specified columns are presented in results
|
||||
:return: :class:`~str`
|
||||
|
||||
The query results is flattened to array:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
[
|
||||
{
|
||||
"_measurement": "mem",
|
||||
"_start": "2021-06-23T06:50:11.897825+00:00",
|
||||
"_stop": "2021-06-25T06:50:11.897825+00:00",
|
||||
"_time": "2020-02-27T16:20:00.897825+00:00",
|
||||
"region": "north",
|
||||
"_field": "usage",
|
||||
"_value": 15
|
||||
},
|
||||
{
|
||||
"_measurement": "mem",
|
||||
"_start": "2021-06-23T06:50:11.897825+00:00",
|
||||
"_stop": "2021-06-25T06:50:11.897825+00:00",
|
||||
"_time": "2020-02-27T16:20:01.897825+00:00",
|
||||
"region": "west",
|
||||
"_field": "usage",
|
||||
"_value": 10
|
||||
},
|
||||
...
|
||||
]
|
||||
|
||||
The JSON format could be configured via ``**kwargs`` arguments:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from influxdb_client import InfluxDBClient
|
||||
|
||||
with InfluxDBClient(url="http://localhost:8086", token="my-token", org="my-org") as client:
|
||||
|
||||
# Query: using Table structure
|
||||
tables = client.query_api().query('from(bucket:"my-bucket") |> range(start: -10m)')
|
||||
|
||||
# Serialize to JSON
|
||||
output = tables.to_json(indent=5)
|
||||
print(output)
|
||||
|
||||
For all available options see - `json.dump <https://docs.python.org/3/library/json.html#json.dump>`_.
|
||||
"""
|
||||
if 'indent' not in kwargs:
|
||||
kwargs['indent'] = 2
|
||||
|
||||
def filter_values(record):
|
||||
if columns is not None:
|
||||
return {k: v for (k, v) in record.values.items() if k in columns}
|
||||
return record.values
|
||||
|
||||
import json
|
||||
return json.dumps(self._to_values(filter_values), cls=FluxStructureEncoder, **kwargs)
|
||||
|
||||
def _to_values(self, mapping):
|
||||
return [mapping(record) for table in self for record in table.records]
|
||||
|
||||
|
||||
class CSVIterator(Iterator[List[str]]):
|
||||
""":class:`Iterator[List[str]]` with additionally functional to better handle of query result."""
|
||||
|
||||
def __init__(self, response: HTTPResponse) -> None:
|
||||
"""Initialize ``csv.reader``."""
|
||||
self.delegate = csv.reader(codecs.iterdecode(response, _UTF_8_encoding))
|
||||
|
||||
def __iter__(self):
|
||||
"""Return an iterator object."""
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
"""Retrieve the next item from the iterator."""
|
||||
row = self.delegate.__next__()
|
||||
while not row:
|
||||
row = self.delegate.__next__()
|
||||
return row
|
||||
|
||||
def to_values(self) -> List[List[str]]:
|
||||
"""
|
||||
Serialize query results to a flattened list of values.
|
||||
|
||||
:return: :class:`~list` of values
|
||||
|
||||
Output example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
[
|
||||
['New York', '2022-06-14T08:00:51.749072045Z', '24.3'],
|
||||
['Prague', '2022-06-14T08:00:51.749072045Z', '25.3'],
|
||||
...
|
||||
]
|
||||
"""
|
||||
return list(self.__iter__())
|
||||
@@ -0,0 +1,438 @@
|
||||
"""InfluxDBClient is client for API defined in https://github.com/influxdata/influxdb/blob/master/http/swagger.yml."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
from influxdb_client import HealthCheck, HealthService, Ready, ReadyService, PingService, \
|
||||
InvokableScriptsApi
|
||||
from influxdb_client.client._base import _BaseClient
|
||||
from influxdb_client.client.authorizations_api import AuthorizationsApi
|
||||
from influxdb_client.client.bucket_api import BucketsApi
|
||||
from influxdb_client.client.delete_api import DeleteApi
|
||||
from influxdb_client.client.labels_api import LabelsApi
|
||||
from influxdb_client.client.organizations_api import OrganizationsApi
|
||||
from influxdb_client.client.query_api import QueryApi, QueryOptions
|
||||
from influxdb_client.client.tasks_api import TasksApi
|
||||
from influxdb_client.client.users_api import UsersApi
|
||||
from influxdb_client.client.write_api import WriteApi, WriteOptions, PointSettings
|
||||
|
||||
logger = logging.getLogger('influxdb_client.client.influxdb_client')
|
||||
|
||||
|
||||
class InfluxDBClient(_BaseClient):
|
||||
"""InfluxDBClient is client for InfluxDB v2."""
|
||||
|
||||
def __init__(self, url, token: str = None, debug=None, timeout=10_000, enable_gzip=False, org: str = None,
|
||||
default_tags: dict = None, **kwargs) -> None:
|
||||
"""
|
||||
Initialize defaults.
|
||||
|
||||
:param url: InfluxDB server API url (ex. http://localhost:8086).
|
||||
:param token: ``token`` to authenticate to the InfluxDB API
|
||||
:param debug: enable verbose logging of http requests
|
||||
:param timeout: HTTP client timeout setting for a request specified in milliseconds.
|
||||
If one number provided, it will be total request timeout.
|
||||
It can also be a pair (tuple) of (connection, read) timeouts.
|
||||
:param enable_gzip: Enable Gzip compression for http requests. Currently, only the "Write" and "Query" endpoints
|
||||
supports the Gzip compression.
|
||||
:param org: organization name (used as a default in Query, Write and Delete API)
|
||||
:key bool verify_ssl: Set this to false to skip verifying SSL certificate when calling API from https server.
|
||||
:key str ssl_ca_cert: Set this to customize the certificate file to verify the peer.
|
||||
:key str cert_file: Path to the certificate that will be used for mTLS authentication.
|
||||
:key str cert_key_file: Path to the file contains private key for mTLS certificate.
|
||||
:key str cert_key_password: String or function which returns password for decrypting the mTLS private key.
|
||||
:key ssl.SSLContext ssl_context: Specify a custom Python SSL Context for the TLS/ mTLS handshake.
|
||||
Be aware that only delivered certificate/ key files or an SSL Context are
|
||||
possible.
|
||||
:key str proxy: Set this to configure the http proxy to be used (ex. http://localhost:3128)
|
||||
:key str proxy_headers: A dictionary containing headers that will be sent to the proxy. Could be used for proxy
|
||||
authentication.
|
||||
:key int connection_pool_maxsize: Number of connections to save that can be reused by urllib3.
|
||||
Defaults to "multiprocessing.cpu_count() * 5".
|
||||
:key urllib3.util.retry.Retry retries: Set the default retry strategy that is used for all HTTP requests
|
||||
except batching writes. As a default there is no one retry strategy.
|
||||
:key bool auth_basic: Set this to true to enable basic authentication when talking to a InfluxDB 1.8.x that
|
||||
does not use auth-enabled but is protected by a reverse proxy with basic authentication.
|
||||
(defaults to false, don't set to true when talking to InfluxDB 2)
|
||||
:key str username: ``username`` to authenticate via username and password credentials to the InfluxDB 2.x
|
||||
:key str password: ``password`` to authenticate via username and password credentials to the InfluxDB 2.x
|
||||
:key list[str] profilers: list of enabled Flux profilers
|
||||
"""
|
||||
super().__init__(url=url, token=token, debug=debug, timeout=timeout, enable_gzip=enable_gzip, org=org,
|
||||
default_tags=default_tags, http_client_logger="urllib3", **kwargs)
|
||||
|
||||
from .._sync.api_client import ApiClient
|
||||
self.api_client = ApiClient(configuration=self.conf, header_name=self.auth_header_name,
|
||||
header_value=self.auth_header_value, retries=self.retries)
|
||||
|
||||
def __enter__(self):
|
||||
"""
|
||||
Enter the runtime context related to this object.
|
||||
|
||||
It will bind this method’s return value to the target(s)
|
||||
specified in the `as` clause of the statement.
|
||||
|
||||
return: self instance
|
||||
"""
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
"""Exit the runtime context related to this object and close the client."""
|
||||
self.close()
|
||||
|
||||
@classmethod
|
||||
def from_config_file(cls, config_file: str = "config.ini", debug=None, enable_gzip=False, **kwargs):
|
||||
"""
|
||||
Configure client via configuration file. The configuration has to be under 'influx' section.
|
||||
|
||||
:param config_file: Path to configuration file
|
||||
:param debug: Enable verbose logging of http requests
|
||||
:param enable_gzip: Enable Gzip compression for http requests. Currently, only the "Write" and "Query" endpoints
|
||||
supports the Gzip compression.
|
||||
:key config_name: Name of the configuration section of the configuration file
|
||||
:key str proxy_headers: A dictionary containing headers that will be sent to the proxy. Could be used for proxy
|
||||
authentication.
|
||||
:key urllib3.util.retry.Retry retries: Set the default retry strategy that is used for all HTTP requests
|
||||
except batching writes. As a default there is no one retry strategy.
|
||||
:key ssl.SSLContext ssl_context: Specify a custom Python SSL Context for the TLS/ mTLS handshake.
|
||||
Be aware that only delivered certificate/ key files or an SSL Context are
|
||||
possible.
|
||||
|
||||
The supported formats:
|
||||
- https://docs.python.org/3/library/configparser.html
|
||||
- https://toml.io/en/
|
||||
- https://www.json.org/json-en.html
|
||||
|
||||
Configuration options:
|
||||
- url
|
||||
- org
|
||||
- token
|
||||
- timeout,
|
||||
- verify_ssl
|
||||
- ssl_ca_cert
|
||||
- cert_file
|
||||
- cert_key_file
|
||||
- cert_key_password
|
||||
- connection_pool_maxsize
|
||||
- auth_basic
|
||||
- profilers
|
||||
- proxy
|
||||
|
||||
|
||||
config.ini example::
|
||||
|
||||
[influx2]
|
||||
url=http://localhost:8086
|
||||
org=my-org
|
||||
token=my-token
|
||||
timeout=6000
|
||||
connection_pool_maxsize=25
|
||||
auth_basic=false
|
||||
profilers=query,operator
|
||||
proxy=http:proxy.domain.org:8080
|
||||
|
||||
[tags]
|
||||
id = 132-987-655
|
||||
customer = California Miner
|
||||
data_center = ${env.data_center}
|
||||
|
||||
config.toml example::
|
||||
|
||||
[influx2]
|
||||
url = "http://localhost:8086"
|
||||
token = "my-token"
|
||||
org = "my-org"
|
||||
timeout = 6000
|
||||
connection_pool_maxsize = 25
|
||||
auth_basic = false
|
||||
profilers="query, operator"
|
||||
proxy = "http://proxy.domain.org:8080"
|
||||
|
||||
[tags]
|
||||
id = "132-987-655"
|
||||
customer = "California Miner"
|
||||
data_center = "${env.data_center}"
|
||||
|
||||
config.json example::
|
||||
|
||||
{
|
||||
"url": "http://localhost:8086",
|
||||
"token": "my-token",
|
||||
"org": "my-org",
|
||||
"active": true,
|
||||
"timeout": 6000,
|
||||
"connection_pool_maxsize": 55,
|
||||
"auth_basic": false,
|
||||
"profilers": "query, operator",
|
||||
"tags": {
|
||||
"id": "132-987-655",
|
||||
"customer": "California Miner",
|
||||
"data_center": "${env.data_center}"
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
return InfluxDBClient._from_config_file(config_file=config_file, debug=debug, enable_gzip=enable_gzip, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_env_properties(cls, debug=None, enable_gzip=False, **kwargs):
|
||||
"""
|
||||
Configure client via environment properties.
|
||||
|
||||
:param debug: Enable verbose logging of http requests
|
||||
:param enable_gzip: Enable Gzip compression for http requests. Currently, only the "Write" and "Query" endpoints
|
||||
supports the Gzip compression.
|
||||
:key str proxy: Set this to configure the http proxy to be used (ex. http://localhost:3128)
|
||||
:key str proxy_headers: A dictionary containing headers that will be sent to the proxy. Could be used for proxy
|
||||
authentication.
|
||||
:key urllib3.util.retry.Retry retries: Set the default retry strategy that is used for all HTTP requests
|
||||
except batching writes. As a default there is no one retry strategy.
|
||||
:key ssl.SSLContext ssl_context: Specify a custom Python SSL Context for the TLS/ mTLS handshake.
|
||||
Be aware that only delivered certificate/ key files or an SSL Context are
|
||||
possible.
|
||||
|
||||
Supported environment properties:
|
||||
- INFLUXDB_V2_URL
|
||||
- INFLUXDB_V2_ORG
|
||||
- INFLUXDB_V2_TOKEN
|
||||
- INFLUXDB_V2_TIMEOUT
|
||||
- INFLUXDB_V2_VERIFY_SSL
|
||||
- INFLUXDB_V2_SSL_CA_CERT
|
||||
- INFLUXDB_V2_CERT_FILE
|
||||
- INFLUXDB_V2_CERT_KEY_FILE
|
||||
- INFLUXDB_V2_CERT_KEY_PASSWORD
|
||||
- INFLUXDB_V2_CONNECTION_POOL_MAXSIZE
|
||||
- INFLUXDB_V2_AUTH_BASIC
|
||||
- INFLUXDB_V2_PROFILERS
|
||||
- INFLUXDB_V2_TAG
|
||||
"""
|
||||
return InfluxDBClient._from_env_properties(debug=debug, enable_gzip=enable_gzip, **kwargs)
|
||||
|
||||
def write_api(self, write_options=WriteOptions(), point_settings=PointSettings(), **kwargs) -> WriteApi:
|
||||
"""
|
||||
Create Write API instance.
|
||||
|
||||
Example:
|
||||
.. code-block:: python
|
||||
|
||||
from influxdb_client import InfluxDBClient
|
||||
from influxdb_client.client.write_api import SYNCHRONOUS
|
||||
|
||||
|
||||
# Initialize SYNCHRONOUS instance of WriteApi
|
||||
with InfluxDBClient(url="http://localhost:8086", token="my-token", org="my-org") as client:
|
||||
write_api = client.write_api(write_options=SYNCHRONOUS)
|
||||
|
||||
If you would like to use a **background batching**, you have to configure client like this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from influxdb_client import InfluxDBClient
|
||||
|
||||
# Initialize background batching instance of WriteApi
|
||||
with InfluxDBClient(url="http://localhost:8086", token="my-token", org="my-org") as client:
|
||||
with client.write_api() as write_api:
|
||||
pass
|
||||
|
||||
There is also possibility to use callbacks to notify about state of background batches:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from influxdb_client import InfluxDBClient
|
||||
from influxdb_client.client.exceptions import InfluxDBError
|
||||
|
||||
|
||||
class BatchingCallback(object):
|
||||
|
||||
def success(self, conf: (str, str, str), data: str):
|
||||
print(f"Written batch: {conf}, data: {data}")
|
||||
|
||||
def error(self, conf: (str, str, str), data: str, exception: InfluxDBError):
|
||||
print(f"Cannot write batch: {conf}, data: {data} due: {exception}")
|
||||
|
||||
def retry(self, conf: (str, str, str), data: str, exception: InfluxDBError):
|
||||
print(f"Retryable error occurs for batch: {conf}, data: {data} retry: {exception}")
|
||||
|
||||
|
||||
with InfluxDBClient(url="http://localhost:8086", token="my-token", org="my-org") as client:
|
||||
callback = BatchingCallback()
|
||||
with client.write_api(success_callback=callback.success,
|
||||
error_callback=callback.error,
|
||||
retry_callback=callback.retry) as write_api:
|
||||
pass
|
||||
|
||||
:param write_options: Write API configuration
|
||||
:param point_settings: settings to store default tags
|
||||
:key success_callback: The callable ``callback`` to run after having successfully written a batch.
|
||||
|
||||
The callable must accept two arguments:
|
||||
- `Tuple`: ``(bucket, organization, precision)``
|
||||
- `str`: written data
|
||||
|
||||
**[batching mode]**
|
||||
|
||||
:key error_callback: The callable ``callback`` to run after having unsuccessfully written a batch.
|
||||
|
||||
The callable must accept three arguments:
|
||||
- `Tuple`: ``(bucket, organization, precision)``
|
||||
- `str`: written data
|
||||
- `Exception`: an occurred error
|
||||
|
||||
**[batching mode]**
|
||||
:key retry_callback: The callable ``callback`` to run after retryable error occurred.
|
||||
|
||||
The callable must accept three arguments:
|
||||
- `Tuple`: ``(bucket, organization, precision)``
|
||||
- `str`: written data
|
||||
- `Exception`: an retryable error
|
||||
|
||||
**[batching mode]**
|
||||
:return: write api instance
|
||||
"""
|
||||
return WriteApi(influxdb_client=self, write_options=write_options, point_settings=point_settings, **kwargs)
|
||||
|
||||
def query_api(self, query_options: QueryOptions = QueryOptions()) -> QueryApi:
|
||||
"""
|
||||
Create an Query API instance.
|
||||
|
||||
:param query_options: optional query api configuration
|
||||
:return: Query api instance
|
||||
"""
|
||||
return QueryApi(self, query_options)
|
||||
|
||||
def invokable_scripts_api(self) -> InvokableScriptsApi:
|
||||
"""
|
||||
Create an InvokableScripts API instance.
|
||||
|
||||
:return: InvokableScripts API instance
|
||||
"""
|
||||
return InvokableScriptsApi(self)
|
||||
|
||||
def close(self):
|
||||
"""Shutdown the client."""
|
||||
self.__del__()
|
||||
|
||||
def __del__(self):
|
||||
"""Shutdown the client."""
|
||||
if self.api_client:
|
||||
self.api_client.__del__()
|
||||
self.api_client = None
|
||||
|
||||
def buckets_api(self) -> BucketsApi:
|
||||
"""
|
||||
Create the Bucket API instance.
|
||||
|
||||
:return: buckets api
|
||||
"""
|
||||
return BucketsApi(self)
|
||||
|
||||
def authorizations_api(self) -> AuthorizationsApi:
|
||||
"""
|
||||
Create the Authorizations API instance.
|
||||
|
||||
:return: authorizations api
|
||||
"""
|
||||
return AuthorizationsApi(self)
|
||||
|
||||
def users_api(self) -> UsersApi:
|
||||
"""
|
||||
Create the Users API instance.
|
||||
|
||||
:return: users api
|
||||
"""
|
||||
return UsersApi(self)
|
||||
|
||||
def organizations_api(self) -> OrganizationsApi:
|
||||
"""
|
||||
Create the Organizations API instance.
|
||||
|
||||
:return: organizations api
|
||||
"""
|
||||
return OrganizationsApi(self)
|
||||
|
||||
def tasks_api(self) -> TasksApi:
|
||||
"""
|
||||
Create the Tasks API instance.
|
||||
|
||||
:return: tasks api
|
||||
"""
|
||||
return TasksApi(self)
|
||||
|
||||
def labels_api(self) -> LabelsApi:
|
||||
"""
|
||||
Create the Labels API instance.
|
||||
|
||||
:return: labels api
|
||||
"""
|
||||
return LabelsApi(self)
|
||||
|
||||
def health(self) -> HealthCheck:
|
||||
"""
|
||||
Get the health of an instance.
|
||||
|
||||
:return: HealthCheck
|
||||
"""
|
||||
warnings.warn("This method is deprecated. Call 'ping()' instead.", DeprecationWarning)
|
||||
health_service = HealthService(self.api_client)
|
||||
|
||||
try:
|
||||
health = health_service.get_health()
|
||||
return health
|
||||
except Exception as e:
|
||||
return HealthCheck(name="influxdb", message=str(e), status="fail")
|
||||
|
||||
def ping(self) -> bool:
|
||||
"""
|
||||
Return the status of InfluxDB instance.
|
||||
|
||||
:return: The status of InfluxDB.
|
||||
"""
|
||||
ping_service = PingService(self.api_client)
|
||||
|
||||
try:
|
||||
ping_service.get_ping()
|
||||
return True
|
||||
except Exception as ex:
|
||||
logger.debug("Unexpected error during /ping: %s", ex)
|
||||
return False
|
||||
|
||||
def version(self) -> str:
|
||||
"""
|
||||
Return the version of the connected InfluxDB Server.
|
||||
|
||||
:return: The version of InfluxDB.
|
||||
"""
|
||||
ping_service = PingService(self.api_client)
|
||||
|
||||
response = ping_service.get_ping_with_http_info(_return_http_data_only=False)
|
||||
|
||||
return ping_service.response_header(response)
|
||||
|
||||
def build(self) -> str:
|
||||
"""
|
||||
Return the build type of the connected InfluxDB Server.
|
||||
|
||||
:return: The type of InfluxDB build.
|
||||
"""
|
||||
ping_service = PingService(self.api_client)
|
||||
|
||||
return ping_service.build_type()
|
||||
|
||||
def ready(self) -> Ready:
|
||||
"""
|
||||
Get The readiness of the InfluxDB 2.0.
|
||||
|
||||
:return: Ready
|
||||
"""
|
||||
ready_service = ReadyService(self.api_client)
|
||||
return ready_service.get_ready()
|
||||
|
||||
def delete_api(self) -> DeleteApi:
|
||||
"""
|
||||
Get the delete metrics API instance.
|
||||
|
||||
:return: delete api
|
||||
"""
|
||||
return DeleteApi(self)
|
||||
@@ -0,0 +1,301 @@
|
||||
"""InfluxDBClientAsync is client for API defined in https://github.com/influxdata/openapi/blob/master/contracts/oss.yml.""" # noqa: E501
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from influxdb_client import PingService
|
||||
from influxdb_client.client._base import _BaseClient
|
||||
from influxdb_client.client.delete_api_async import DeleteApiAsync
|
||||
from influxdb_client.client.query_api import QueryOptions
|
||||
from influxdb_client.client.query_api_async import QueryApiAsync
|
||||
from influxdb_client.client.write_api import PointSettings
|
||||
from influxdb_client.client.write_api_async import WriteApiAsync
|
||||
|
||||
logger = logging.getLogger('influxdb_client.client.influxdb_client_async')
|
||||
|
||||
|
||||
class InfluxDBClientAsync(_BaseClient):
|
||||
"""InfluxDBClientAsync is client for InfluxDB v2."""
|
||||
|
||||
def __init__(self, url, token: str = None, org: str = None, debug=None, timeout=10_000, enable_gzip=False,
|
||||
**kwargs) -> None:
|
||||
"""
|
||||
Initialize defaults.
|
||||
|
||||
:param url: InfluxDB server API url (ex. http://localhost:8086).
|
||||
:param token: ``token`` to authenticate to the InfluxDB 2.x
|
||||
:param org: organization name (used as a default in Query, Write and Delete API)
|
||||
:param debug: enable verbose logging of http requests
|
||||
:param timeout: The maximal number of milliseconds for the whole HTTP request including
|
||||
connection establishment, request sending and response reading.
|
||||
It can also be a :class:`~aiohttp.ClientTimeout` which is directly pass to ``aiohttp``.
|
||||
:param enable_gzip: Enable Gzip compression for http requests. Currently, only the "Write" and "Query" endpoints
|
||||
supports the Gzip compression.
|
||||
:key bool verify_ssl: Set this to false to skip verifying SSL certificate when calling API from https server.
|
||||
:key str ssl_ca_cert: Set this to customize the certificate file to verify the peer.
|
||||
:key str cert_file: Path to the certificate that will be used for mTLS authentication.
|
||||
:key str cert_key_file: Path to the file contains private key for mTLS certificate.
|
||||
:key str cert_key_password: String or function which returns password for decrypting the mTLS private key.
|
||||
:key ssl.SSLContext ssl_context: Specify a custom Python SSL Context for the TLS/ mTLS handshake.
|
||||
Be aware that only delivered certificate/ key files or an SSL Context are
|
||||
possible.
|
||||
:key str proxy: Set this to configure the http proxy to be used (ex. http://localhost:3128)
|
||||
:key str proxy_headers: A dictionary containing headers that will be sent to the proxy. Could be used for proxy
|
||||
authentication.
|
||||
:key int connection_pool_maxsize: The total number of simultaneous connections.
|
||||
Defaults to "multiprocessing.cpu_count() * 5".
|
||||
:key bool auth_basic: Set this to true to enable basic authentication when talking to a InfluxDB 1.8.x that
|
||||
does not use auth-enabled but is protected by a reverse proxy with basic authentication.
|
||||
(defaults to false, don't set to true when talking to InfluxDB 2)
|
||||
:key str username: ``username`` to authenticate via username and password credentials to the InfluxDB 2.x
|
||||
:key str password: ``password`` to authenticate via username and password credentials to the InfluxDB 2.x
|
||||
:key bool allow_redirects: If set to ``False``, do not follow HTTP redirects. ``True`` by default.
|
||||
:key int max_redirects: Maximum number of HTTP redirects to follow. ``10`` by default.
|
||||
:key dict client_session_kwargs: Additional configuration arguments for :class:`~aiohttp.ClientSession`
|
||||
:key type client_session_type: Type of aiohttp client to use. Useful for third party wrappers like
|
||||
``aiohttp-retry``. :class:`~aiohttp.ClientSession` by default.
|
||||
:key list[str] profilers: list of enabled Flux profilers
|
||||
"""
|
||||
super().__init__(url=url, token=token, org=org, debug=debug, timeout=timeout, enable_gzip=enable_gzip,
|
||||
http_client_logger="aiohttp.client", **kwargs)
|
||||
|
||||
# compatibility with Python 3.6
|
||||
if sys.version_info[:2] >= (3, 7):
|
||||
from asyncio import get_running_loop
|
||||
else:
|
||||
from asyncio import _get_running_loop as get_running_loop
|
||||
|
||||
# check present asynchronous context
|
||||
try:
|
||||
loop = get_running_loop()
|
||||
# compatibility with Python 3.6
|
||||
if loop is None:
|
||||
raise RuntimeError('no running event loop')
|
||||
except RuntimeError:
|
||||
from influxdb_client.client.exceptions import InfluxDBError
|
||||
message = "The async client should be initialised inside async coroutine " \
|
||||
"otherwise there can be unexpected behaviour."
|
||||
raise InfluxDBError(response=None, message=message)
|
||||
|
||||
from .._async.api_client import ApiClientAsync
|
||||
self.api_client = ApiClientAsync(configuration=self.conf, header_name=self.auth_header_name,
|
||||
header_value=self.auth_header_value, **kwargs)
|
||||
|
||||
async def __aenter__(self) -> 'InfluxDBClientAsync':
|
||||
"""
|
||||
Enter the runtime context related to this object.
|
||||
|
||||
return: self instance
|
||||
"""
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb) -> None:
|
||||
"""Shutdown the client."""
|
||||
await self.close()
|
||||
|
||||
async def close(self):
|
||||
"""Shutdown the client."""
|
||||
if self.api_client:
|
||||
await self.api_client.close()
|
||||
self.api_client = None
|
||||
|
||||
@classmethod
|
||||
def from_config_file(cls, config_file: str = "config.ini", debug=None, enable_gzip=False, **kwargs):
|
||||
"""
|
||||
Configure client via configuration file. The configuration has to be under 'influx' section.
|
||||
|
||||
:param config_file: Path to configuration file
|
||||
:param debug: Enable verbose logging of http requests
|
||||
:param enable_gzip: Enable Gzip compression for http requests. Currently, only the "Write" and "Query" endpoints
|
||||
supports the Gzip compression.
|
||||
:key config_name: Name of the configuration section of the configuration file
|
||||
:key str proxy_headers: A dictionary containing headers that will be sent to the proxy. Could be used for proxy
|
||||
authentication.
|
||||
:key urllib3.util.retry.Retry retries: Set the default retry strategy that is used for all HTTP requests
|
||||
except batching writes. As a default there is no one retry strategy.
|
||||
:key ssl.SSLContext ssl_context: Specify a custom Python SSL Context for the TLS/ mTLS handshake.
|
||||
Be aware that only delivered certificate/ key files or an SSL Context are
|
||||
possible.
|
||||
|
||||
The supported formats:
|
||||
- https://docs.python.org/3/library/configparser.html
|
||||
- https://toml.io/en/
|
||||
- https://www.json.org/json-en.html
|
||||
|
||||
Configuration options:
|
||||
- url
|
||||
- org
|
||||
- token
|
||||
- timeout,
|
||||
- verify_ssl
|
||||
- ssl_ca_cert
|
||||
- cert_file
|
||||
- cert_key_file
|
||||
- cert_key_password
|
||||
- connection_pool_maxsize
|
||||
- auth_basic
|
||||
- profilers
|
||||
- proxy
|
||||
|
||||
|
||||
config.ini example::
|
||||
|
||||
[influx2]
|
||||
url=http://localhost:8086
|
||||
org=my-org
|
||||
token=my-token
|
||||
timeout=6000
|
||||
connection_pool_maxsize=25
|
||||
auth_basic=false
|
||||
profilers=query,operator
|
||||
proxy=http:proxy.domain.org:8080
|
||||
|
||||
[tags]
|
||||
id = 132-987-655
|
||||
customer = California Miner
|
||||
data_center = ${env.data_center}
|
||||
|
||||
config.toml example::
|
||||
|
||||
[influx2]
|
||||
url = "http://localhost:8086"
|
||||
token = "my-token"
|
||||
org = "my-org"
|
||||
timeout = 6000
|
||||
connection_pool_maxsize = 25
|
||||
auth_basic = false
|
||||
profilers="query, operator"
|
||||
proxy = "http://proxy.domain.org:8080"
|
||||
|
||||
[tags]
|
||||
id = "132-987-655"
|
||||
customer = "California Miner"
|
||||
data_center = "${env.data_center}"
|
||||
|
||||
config.json example::
|
||||
|
||||
{
|
||||
"url": "http://localhost:8086",
|
||||
"token": "my-token",
|
||||
"org": "my-org",
|
||||
"active": true,
|
||||
"timeout": 6000,
|
||||
"connection_pool_maxsize": 55,
|
||||
"auth_basic": false,
|
||||
"profilers": "query, operator",
|
||||
"tags": {
|
||||
"id": "132-987-655",
|
||||
"customer": "California Miner",
|
||||
"data_center": "${env.data_center}"
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
return InfluxDBClientAsync._from_config_file(config_file=config_file, debug=debug,
|
||||
enable_gzip=enable_gzip, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_env_properties(cls, debug=None, enable_gzip=False, **kwargs):
|
||||
"""
|
||||
Configure client via environment properties.
|
||||
|
||||
:param debug: Enable verbose logging of http requests
|
||||
:param enable_gzip: Enable Gzip compression for http requests. Currently, only the "Write" and "Query" endpoints
|
||||
supports the Gzip compression.
|
||||
:key str proxy: Set this to configure the http proxy to be used (ex. http://localhost:3128)
|
||||
:key str proxy_headers: A dictionary containing headers that will be sent to the proxy. Could be used for proxy
|
||||
authentication.
|
||||
:key urllib3.util.retry.Retry retries: Set the default retry strategy that is used for all HTTP requests
|
||||
except batching writes. As a default there is no one retry strategy.
|
||||
:key ssl.SSLContext ssl_context: Specify a custom Python SSL Context for the TLS/ mTLS handshake.
|
||||
Be aware that only delivered certificate/ key files or an SSL Context are
|
||||
possible.
|
||||
|
||||
|
||||
Supported environment properties:
|
||||
- INFLUXDB_V2_URL
|
||||
- INFLUXDB_V2_ORG
|
||||
- INFLUXDB_V2_TOKEN
|
||||
- INFLUXDB_V2_TIMEOUT
|
||||
- INFLUXDB_V2_VERIFY_SSL
|
||||
- INFLUXDB_V2_SSL_CA_CERT
|
||||
- INFLUXDB_V2_CERT_FILE
|
||||
- INFLUXDB_V2_CERT_KEY_FILE
|
||||
- INFLUXDB_V2_CERT_KEY_PASSWORD
|
||||
- INFLUXDB_V2_CONNECTION_POOL_MAXSIZE
|
||||
- INFLUXDB_V2_AUTH_BASIC
|
||||
- INFLUXDB_V2_PROFILERS
|
||||
- INFLUXDB_V2_TAG
|
||||
"""
|
||||
return InfluxDBClientAsync._from_env_properties(debug=debug, enable_gzip=enable_gzip, **kwargs)
|
||||
|
||||
async def ping(self) -> bool:
|
||||
"""
|
||||
Return the status of InfluxDB instance.
|
||||
|
||||
:return: The status of InfluxDB.
|
||||
"""
|
||||
ping_service = PingService(self.api_client)
|
||||
|
||||
try:
|
||||
await ping_service.get_ping_async()
|
||||
return True
|
||||
except Exception as ex:
|
||||
logger.debug("Unexpected error during /ping: %s", ex)
|
||||
raise ex
|
||||
|
||||
async def version(self) -> str:
|
||||
"""
|
||||
Return the version of the connected InfluxDB Server.
|
||||
|
||||
:return: The version of InfluxDB.
|
||||
"""
|
||||
ping_service = PingService(self.api_client)
|
||||
|
||||
response = await ping_service.get_ping_async(_return_http_data_only=False)
|
||||
return ping_service.response_header(response)
|
||||
|
||||
async def build(self) -> str:
|
||||
"""
|
||||
Return the build type of the connected InfluxDB Server.
|
||||
|
||||
:return: The type of InfluxDB build.
|
||||
"""
|
||||
ping_service = PingService(self.api_client)
|
||||
|
||||
return await ping_service.build_type_async()
|
||||
|
||||
def query_api(self, query_options: QueryOptions = QueryOptions()) -> QueryApiAsync:
|
||||
"""
|
||||
Create an asynchronous Query API instance.
|
||||
|
||||
:param query_options: optional query api configuration
|
||||
:return: Query api instance
|
||||
"""
|
||||
return QueryApiAsync(self, query_options)
|
||||
|
||||
def write_api(self, point_settings=PointSettings()) -> WriteApiAsync:
|
||||
"""
|
||||
Create an asynchronous Write API instance.
|
||||
|
||||
Example:
|
||||
.. code-block:: python
|
||||
|
||||
from influxdb_client_async import InfluxDBClientAsync
|
||||
|
||||
|
||||
# Initialize async/await instance of Write API
|
||||
async with InfluxDBClientAsync(url="http://localhost:8086", token="my-token", org="my-org") as client:
|
||||
write_api = client.write_api()
|
||||
|
||||
:param point_settings: settings to store default tags
|
||||
:return: write api instance
|
||||
"""
|
||||
return WriteApiAsync(influxdb_client=self, point_settings=point_settings)
|
||||
|
||||
def delete_api(self) -> DeleteApiAsync:
|
||||
"""
|
||||
Get the asynchronous delete metrics API instance.
|
||||
|
||||
:return: delete api
|
||||
"""
|
||||
return DeleteApiAsync(self)
|
||||
@@ -0,0 +1,293 @@
|
||||
"""
|
||||
Use API invokable scripts to create custom InfluxDB API endpoints that query, process, and shape data.
|
||||
|
||||
API invokable scripts let you assign scripts to API endpoints and then execute them as standard REST operations
|
||||
in InfluxDB Cloud.
|
||||
"""
|
||||
|
||||
from typing import List, Iterator, Generator, Any
|
||||
|
||||
from influxdb_client import Script, InvokableScriptsService, ScriptCreateRequest, ScriptUpdateRequest, \
|
||||
ScriptInvocationParams
|
||||
from influxdb_client.client._base import _BaseQueryApi
|
||||
from influxdb_client.client.flux_csv_parser import FluxResponseMetadataMode
|
||||
from influxdb_client.client.flux_table import FluxRecord, TableList, CSVIterator
|
||||
|
||||
|
||||
class InvokableScriptsApi(_BaseQueryApi):
|
||||
"""Use API invokable scripts to create custom InfluxDB API endpoints that query, process, and shape data."""
|
||||
|
||||
def __init__(self, influxdb_client):
|
||||
"""Initialize defaults."""
|
||||
self._influxdb_client = influxdb_client
|
||||
self._invokable_scripts_service = InvokableScriptsService(influxdb_client.api_client)
|
||||
|
||||
def create_script(self, create_request: ScriptCreateRequest) -> Script:
|
||||
"""Create a script.
|
||||
|
||||
:param ScriptCreateRequest create_request: The script to create. (required)
|
||||
:return: The created script.
|
||||
"""
|
||||
return self._invokable_scripts_service.post_scripts(script_create_request=create_request)
|
||||
|
||||
def update_script(self, script_id: str, update_request: ScriptUpdateRequest) -> Script:
|
||||
"""Update a script.
|
||||
|
||||
:param str script_id: The ID of the script to update. (required)
|
||||
:param ScriptUpdateRequest update_request: Script updates to apply (required)
|
||||
:return: The updated.
|
||||
"""
|
||||
return self._invokable_scripts_service.patch_scripts_id(script_id=script_id,
|
||||
script_update_request=update_request)
|
||||
|
||||
def delete_script(self, script_id: str) -> None:
|
||||
"""Delete a script.
|
||||
|
||||
:param str script_id: The ID of the script to delete. (required)
|
||||
:return: None
|
||||
"""
|
||||
self._invokable_scripts_service.delete_scripts_id(script_id=script_id)
|
||||
|
||||
def find_scripts(self, **kwargs):
|
||||
"""List scripts.
|
||||
|
||||
:key int limit: The number of scripts to return.
|
||||
:key int offset: The offset for pagination.
|
||||
:return: List of scripts.
|
||||
:rtype: list[Script]
|
||||
"""
|
||||
return self._invokable_scripts_service.get_scripts(**kwargs).scripts
|
||||
|
||||
def invoke_script(self, script_id: str, params: dict = None) -> TableList:
|
||||
"""
|
||||
Invoke synchronously a script and return result as a TableList.
|
||||
|
||||
The bind parameters referenced in the script are substitutes with `params` key-values sent in the request body.
|
||||
|
||||
:param str script_id: The ID of the script to invoke. (required)
|
||||
:param params: bind parameters
|
||||
:return: :class:`~influxdb_client.client.flux_table.FluxTable` list wrapped into
|
||||
:class:`~influxdb_client.client.flux_table.TableList`
|
||||
:rtype: TableList
|
||||
|
||||
Serialization the query results to flattened list of values via :func:`~influxdb_client.client.flux_table.TableList.to_values`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from influxdb_client import InfluxDBClient
|
||||
|
||||
with InfluxDBClient(url="https://us-west-2-1.aws.cloud2.influxdata.com", token="my-token", org="my-org") as client:
|
||||
|
||||
# Query: using Table structure
|
||||
tables = client.invokable_scripts_api().invoke_script(script_id="script-id")
|
||||
|
||||
# Serialize to values
|
||||
output = tables.to_values(columns=['location', '_time', '_value'])
|
||||
print(output)
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
[
|
||||
['New York', datetime.datetime(2022, 6, 7, 11, 3, 22, 917593, tzinfo=tzutc()), 24.3],
|
||||
['Prague', datetime.datetime(2022, 6, 7, 11, 3, 22, 917593, tzinfo=tzutc()), 25.3],
|
||||
...
|
||||
]
|
||||
|
||||
Serialization the query results to JSON via :func:`~influxdb_client.client.flux_table.TableList.to_json`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from influxdb_client import InfluxDBClient
|
||||
|
||||
with InfluxDBClient(url="https://us-west-2-1.aws.cloud2.influxdata.com", token="my-token", org="my-org") as client:
|
||||
|
||||
# Query: using Table structure
|
||||
tables = client.invokable_scripts_api().invoke_script(script_id="script-id")
|
||||
|
||||
# Serialize to JSON
|
||||
output = tables.to_json(indent=5)
|
||||
print(output)
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
[
|
||||
{
|
||||
"_measurement": "mem",
|
||||
"_start": "2021-06-23T06:50:11.897825+00:00",
|
||||
"_stop": "2021-06-25T06:50:11.897825+00:00",
|
||||
"_time": "2020-02-27T16:20:00.897825+00:00",
|
||||
"region": "north",
|
||||
"_field": "usage",
|
||||
"_value": 15
|
||||
},
|
||||
{
|
||||
"_measurement": "mem",
|
||||
"_start": "2021-06-23T06:50:11.897825+00:00",
|
||||
"_stop": "2021-06-25T06:50:11.897825+00:00",
|
||||
"_time": "2020-02-27T16:20:01.897825+00:00",
|
||||
"region": "west",
|
||||
"_field": "usage",
|
||||
"_value": 10
|
||||
},
|
||||
...
|
||||
]
|
||||
""" # noqa: E501
|
||||
response = self._invokable_scripts_service \
|
||||
.post_scripts_id_invoke(script_id=script_id,
|
||||
script_invocation_params=ScriptInvocationParams(params=params),
|
||||
async_req=False,
|
||||
_preload_content=False,
|
||||
_return_http_data_only=False)
|
||||
return self._to_tables(response, query_options=None, response_metadata_mode=FluxResponseMetadataMode.only_names)
|
||||
|
||||
def invoke_script_stream(self, script_id: str, params: dict = None) -> Generator['FluxRecord', Any, None]:
|
||||
"""
|
||||
Invoke synchronously a script and return result as a Generator['FluxRecord'].
|
||||
|
||||
The bind parameters referenced in the script are substitutes with `params` key-values sent in the request body.
|
||||
|
||||
:param str script_id: The ID of the script to invoke. (required)
|
||||
:param params: bind parameters
|
||||
:return: Stream of FluxRecord.
|
||||
:rtype: Generator['FluxRecord']
|
||||
"""
|
||||
response = self._invokable_scripts_service \
|
||||
.post_scripts_id_invoke(script_id=script_id,
|
||||
script_invocation_params=ScriptInvocationParams(params=params),
|
||||
async_req=False,
|
||||
_preload_content=False,
|
||||
_return_http_data_only=False)
|
||||
|
||||
return self._to_flux_record_stream(response, query_options=None,
|
||||
response_metadata_mode=FluxResponseMetadataMode.only_names)
|
||||
|
||||
def invoke_script_data_frame(self, script_id: str, params: dict = None, data_frame_index: List[str] = None):
|
||||
"""
|
||||
Invoke synchronously a script and return Pandas DataFrame.
|
||||
|
||||
The bind parameters referenced in the script are substitutes with `params` key-values sent in the request body.
|
||||
|
||||
.. note:: If the ``script`` returns tables with differing schemas than the client generates a :class:`~DataFrame` for each of them.
|
||||
|
||||
:param str script_id: The ID of the script to invoke. (required)
|
||||
:param List[str] data_frame_index: The list of columns that are used as DataFrame index.
|
||||
:param params: bind parameters
|
||||
:return: :class:`~DataFrame` or :class:`~List[DataFrame]`
|
||||
|
||||
.. warning:: For the optimal processing of the query results use the ``pivot() function`` which align results as a table.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
from(bucket:"my-bucket")
|
||||
|> range(start: -5m, stop: now())
|
||||
|> filter(fn: (r) => r._measurement == "mem")
|
||||
|> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
|
||||
|
||||
For more info see:
|
||||
- https://docs.influxdata.com/resources/videos/pivots-in-flux/
|
||||
- https://docs.influxdata.com/flux/latest/stdlib/universe/pivot/
|
||||
- https://docs.influxdata.com/flux/latest/stdlib/influxdata/influxdb/schema/fieldsascols/
|
||||
""" # noqa: E501
|
||||
_generator = self.invoke_script_data_frame_stream(script_id=script_id,
|
||||
params=params,
|
||||
data_frame_index=data_frame_index)
|
||||
return self._to_data_frames(_generator)
|
||||
|
||||
def invoke_script_data_frame_stream(self, script_id: str, params: dict = None, data_frame_index: List[str] = None):
|
||||
"""
|
||||
Invoke synchronously a script and return stream of Pandas DataFrame as a Generator['pd.DataFrame'].
|
||||
|
||||
The bind parameters referenced in the script are substitutes with `params` key-values sent in the request body.
|
||||
|
||||
.. note:: If the ``script`` returns tables with differing schemas than the client generates a :class:`~DataFrame` for each of them.
|
||||
|
||||
:param str script_id: The ID of the script to invoke. (required)
|
||||
:param List[str] data_frame_index: The list of columns that are used as DataFrame index.
|
||||
:param params: bind parameters
|
||||
:return: :class:`~Generator[DataFrame]`
|
||||
|
||||
.. warning:: For the optimal processing of the query results use the ``pivot() function`` which align results as a table.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
from(bucket:"my-bucket")
|
||||
|> range(start: -5m, stop: now())
|
||||
|> filter(fn: (r) => r._measurement == "mem")
|
||||
|> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
|
||||
|
||||
For more info see:
|
||||
- https://docs.influxdata.com/resources/videos/pivots-in-flux/
|
||||
- https://docs.influxdata.com/flux/latest/stdlib/universe/pivot/
|
||||
- https://docs.influxdata.com/flux/latest/stdlib/influxdata/influxdb/schema/fieldsascols/
|
||||
""" # noqa: E501
|
||||
response = self._invokable_scripts_service \
|
||||
.post_scripts_id_invoke(script_id=script_id,
|
||||
script_invocation_params=ScriptInvocationParams(params=params),
|
||||
async_req=False,
|
||||
_preload_content=False,
|
||||
_return_http_data_only=False)
|
||||
|
||||
return self._to_data_frame_stream(data_frame_index, response, query_options=None,
|
||||
response_metadata_mode=FluxResponseMetadataMode.only_names)
|
||||
|
||||
def invoke_script_csv(self, script_id: str, params: dict = None) -> CSVIterator:
|
||||
"""
|
||||
Invoke synchronously a script and return result as a CSV iterator. Each iteration returns a row of the CSV file.
|
||||
|
||||
The bind parameters referenced in the script are substitutes with `params` key-values sent in the request body.
|
||||
|
||||
:param str script_id: The ID of the script to invoke. (required)
|
||||
:param params: bind parameters
|
||||
:return: :class:`~Iterator[List[str]]` wrapped into :class:`~influxdb_client.client.flux_table.CSVIterator`
|
||||
:rtype: CSVIterator
|
||||
|
||||
Serialization the query results to flattened list of values via :func:`~influxdb_client.client.flux_table.CSVIterator.to_values`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from influxdb_client import InfluxDBClient
|
||||
|
||||
with InfluxDBClient(url="http://localhost:8086", token="my-token", org="my-org") as client:
|
||||
|
||||
# Query: using CSV iterator
|
||||
csv_iterator = client.invokable_scripts_api().invoke_script_csv(script_id="script-id")
|
||||
|
||||
# Serialize to values
|
||||
output = csv_iterator.to_values()
|
||||
print(output)
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
[
|
||||
['', 'result', 'table', '_start', '_stop', '_time', '_value', '_field', '_measurement', 'location']
|
||||
['', '', '0', '2022-06-16', '2022-06-16', '2022-06-16', '24.3', 'temperature', 'my_measurement', 'New York']
|
||||
['', '', '1', '2022-06-16', '2022-06-16', '2022-06-16', '25.3', 'temperature', 'my_measurement', 'Prague']
|
||||
...
|
||||
]
|
||||
|
||||
""" # noqa: E501
|
||||
response = self._invokable_scripts_service \
|
||||
.post_scripts_id_invoke(script_id=script_id,
|
||||
script_invocation_params=ScriptInvocationParams(params=params),
|
||||
async_req=False,
|
||||
_preload_content=False)
|
||||
|
||||
return self._to_csv(response)
|
||||
|
||||
def invoke_script_raw(self, script_id: str, params: dict = None) -> Iterator[List[str]]:
|
||||
"""
|
||||
Invoke synchronously a script and return result as raw unprocessed result as a str.
|
||||
|
||||
The bind parameters referenced in the script are substitutes with `params` key-values sent in the request body.
|
||||
|
||||
:param str script_id: The ID of the script to invoke. (required)
|
||||
:param params: bind parameters
|
||||
:return: Result as a str.
|
||||
"""
|
||||
response = self._invokable_scripts_service \
|
||||
.post_scripts_id_invoke(script_id=script_id,
|
||||
script_invocation_params=ScriptInvocationParams(params=params),
|
||||
async_req=False,
|
||||
_preload_content=True)
|
||||
|
||||
return response
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Labels are a way to add visual metadata to dashboards, tasks, and other items in the InfluxDB UI."""
|
||||
|
||||
from typing import List, Dict, Union
|
||||
|
||||
from influxdb_client import LabelsService, LabelCreateRequest, Label, LabelUpdate
|
||||
|
||||
|
||||
class LabelsApi(object):
|
||||
"""Implementation for '/api/v2/labels' endpoint."""
|
||||
|
||||
def __init__(self, influxdb_client):
|
||||
"""Initialize defaults."""
|
||||
self._influxdb_client = influxdb_client
|
||||
self._service = LabelsService(influxdb_client.api_client)
|
||||
|
||||
def create_label(self, name: str, org_id: str, properties: Dict[str, str] = None) -> Label:
|
||||
"""
|
||||
Create a new label.
|
||||
|
||||
:param name: label name
|
||||
:param org_id: organization id
|
||||
:param properties: optional label properties
|
||||
:return: created label
|
||||
"""
|
||||
label_request = LabelCreateRequest(org_id=org_id, name=name, properties=properties)
|
||||
return self._service.post_labels(label_create_request=label_request).label
|
||||
|
||||
def update_label(self, label: Label):
|
||||
"""
|
||||
Update an existing label name and properties.
|
||||
|
||||
:param label: label
|
||||
:return: the updated label
|
||||
"""
|
||||
label_update = LabelUpdate()
|
||||
label_update.properties = label.properties
|
||||
label_update.name = label.name
|
||||
return self._service.patch_labels_id(label_id=label.id, label_update=label_update).label
|
||||
|
||||
def delete_label(self, label: Union[str, Label]):
|
||||
"""
|
||||
Delete the label.
|
||||
|
||||
:param label: label id or Label
|
||||
"""
|
||||
label_id = None
|
||||
|
||||
if isinstance(label, str):
|
||||
label_id = label
|
||||
|
||||
if isinstance(label, Label):
|
||||
label_id = label.id
|
||||
|
||||
return self._service.delete_labels_id(label_id=label_id)
|
||||
|
||||
def clone_label(self, cloned_name: str, label: Label) -> Label:
|
||||
"""
|
||||
Create the new instance of the label as a copy existing label.
|
||||
|
||||
:param cloned_name: new label name
|
||||
:param label: existing label
|
||||
:return: clonned Label
|
||||
"""
|
||||
cloned_properties = None
|
||||
if label.properties is not None:
|
||||
cloned_properties = label.properties.copy()
|
||||
|
||||
return self.create_label(name=cloned_name, properties=cloned_properties, org_id=label.org_id)
|
||||
|
||||
def find_labels(self, **kwargs) -> List['Label']:
|
||||
"""
|
||||
Get all available labels.
|
||||
|
||||
:key str org_id: The organization ID.
|
||||
|
||||
:return: labels
|
||||
"""
|
||||
return self._service.get_labels(**kwargs).labels
|
||||
|
||||
def find_label_by_id(self, label_id: str):
|
||||
"""
|
||||
Retrieve the label by id.
|
||||
|
||||
:param label_id:
|
||||
:return: Label
|
||||
"""
|
||||
return self._service.get_labels_id(label_id=label_id).label
|
||||
|
||||
def find_label_by_org(self, org_id) -> List['Label']:
|
||||
"""
|
||||
Get the list of all labels for given organization.
|
||||
|
||||
:param org_id: organization id
|
||||
:return: list of labels
|
||||
"""
|
||||
return self._service.get_labels(org_id=org_id).labels
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Use the influxdb_client with python native logging."""
|
||||
import logging
|
||||
|
||||
from influxdb_client import InfluxDBClient
|
||||
|
||||
|
||||
class InfluxLoggingHandler(logging.Handler):
|
||||
"""
|
||||
InfluxLoggingHandler instances dispatch logging events to influx.
|
||||
|
||||
There is no need to set a Formatter.
|
||||
The raw input will be passed on to the influx write api.
|
||||
"""
|
||||
|
||||
DEFAULT_LOG_RECORD_KEYS = list(logging.makeLogRecord({}).__dict__.keys()) + ['message']
|
||||
|
||||
def __init__(self, *, url, token, org, bucket, client_args=None, write_api_args=None):
|
||||
"""
|
||||
Initialize defaults.
|
||||
|
||||
The arguments `client_args` and `write_api_args` can be dicts of kwargs.
|
||||
They are passed on to the InfluxDBClient and write_api calls respectively.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self.bucket = bucket
|
||||
|
||||
client_args = {} if client_args is None else client_args
|
||||
self.client = InfluxDBClient(url=url, token=token, org=org, **client_args)
|
||||
|
||||
write_api_args = {} if write_api_args is None else write_api_args
|
||||
self.write_api = self.client.write_api(**write_api_args)
|
||||
|
||||
def __del__(self):
|
||||
"""Make sure all resources are closed."""
|
||||
self.close()
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the write_api, client and logger."""
|
||||
self.write_api.close()
|
||||
self.client.close()
|
||||
super().close()
|
||||
|
||||
def emit(self, record: logging.LogRecord) -> None:
|
||||
"""Emit a record via the influxDB WriteApi."""
|
||||
try:
|
||||
message = self.format(record)
|
||||
extra = self._get_extra_values(record)
|
||||
return self.write_api.write(record=message, **extra)
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except (Exception,):
|
||||
self.handleError(record)
|
||||
|
||||
def _get_extra_values(self, record: logging.LogRecord) -> dict:
|
||||
"""
|
||||
Extract all items from the record that were injected via extra.
|
||||
|
||||
Example: `logging.debug(msg, extra={key: value, ...})`.
|
||||
"""
|
||||
extra = {'bucket': self.bucket}
|
||||
extra.update({key: value for key, value in record.__dict__.items()
|
||||
if key not in self.DEFAULT_LOG_RECORD_KEYS})
|
||||
return extra
|
||||
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
An organization is a workspace for a group of users.
|
||||
|
||||
All dashboards, tasks, buckets, members, etc., belong to an organization.
|
||||
"""
|
||||
|
||||
from influxdb_client import OrganizationsService, UsersService, Organization, PatchOrganizationRequest
|
||||
|
||||
|
||||
class OrganizationsApi(object):
|
||||
"""Implementation for '/api/v2/orgs' endpoint."""
|
||||
|
||||
def __init__(self, influxdb_client):
|
||||
"""Initialize defaults."""
|
||||
self._influxdb_client = influxdb_client
|
||||
self._organizations_service = OrganizationsService(influxdb_client.api_client)
|
||||
self._users_service = UsersService(influxdb_client.api_client)
|
||||
|
||||
def me(self):
|
||||
"""Return the current authenticated user."""
|
||||
user = self._users_service.get_me()
|
||||
return user
|
||||
|
||||
def find_organization(self, org_id):
|
||||
"""Retrieve an organization."""
|
||||
return self._organizations_service.get_orgs_id(org_id=org_id)
|
||||
|
||||
def find_organizations(self, **kwargs):
|
||||
"""
|
||||
List all organizations.
|
||||
|
||||
:key int offset: Offset for pagination
|
||||
:key int limit: Limit for pagination
|
||||
:key bool descending:
|
||||
:key str org: Filter organizations to a specific organization name.
|
||||
:key str org_id: Filter organizations to a specific organization ID.
|
||||
:key str user_id: Filter organizations to a specific user ID.
|
||||
"""
|
||||
return self._organizations_service.get_orgs(**kwargs).orgs
|
||||
|
||||
def create_organization(self, name: str = None, organization: Organization = None) -> Organization:
|
||||
"""Create an organization."""
|
||||
if organization is None:
|
||||
organization = Organization(name=name)
|
||||
return self._organizations_service.post_orgs(post_organization_request=organization)
|
||||
|
||||
def update_organization(self, organization: Organization) -> Organization:
|
||||
"""Update an organization.
|
||||
|
||||
:param organization: Organization update to apply (required)
|
||||
:return: Organization
|
||||
"""
|
||||
request = PatchOrganizationRequest(name=organization.name,
|
||||
description=organization.description)
|
||||
|
||||
return self._organizations_service.patch_orgs_id(org_id=organization.id, patch_organization_request=request)
|
||||
|
||||
def delete_organization(self, org_id: str):
|
||||
"""Delete an organization."""
|
||||
return self._organizations_service.delete_orgs_id(org_id=org_id)
|
||||
@@ -0,0 +1,310 @@
|
||||
"""
|
||||
Querying InfluxDB by FluxLang.
|
||||
|
||||
Flux is InfluxData’s functional data scripting language designed for querying, analyzing, and acting on data.
|
||||
"""
|
||||
|
||||
from typing import List, Generator, Any, Callable
|
||||
|
||||
from influxdb_client import Dialect
|
||||
from influxdb_client.client._base import _BaseQueryApi
|
||||
from influxdb_client.client.flux_table import FluxRecord, TableList, CSVIterator
|
||||
|
||||
|
||||
class QueryOptions(object):
|
||||
"""Query options."""
|
||||
|
||||
def __init__(self, profilers: List[str] = None, profiler_callback: Callable = None) -> None:
|
||||
"""
|
||||
Initialize query options.
|
||||
|
||||
:param profilers: list of enabled flux profilers
|
||||
:param profiler_callback: callback function return profilers (FluxRecord)
|
||||
"""
|
||||
self.profilers = profilers
|
||||
self.profiler_callback = profiler_callback
|
||||
|
||||
|
||||
class QueryApi(_BaseQueryApi):
|
||||
"""Implementation for '/api/v2/query' endpoint."""
|
||||
|
||||
def __init__(self, influxdb_client, query_options=QueryOptions()):
|
||||
"""
|
||||
Initialize query client.
|
||||
|
||||
:param influxdb_client: influxdb client
|
||||
"""
|
||||
super().__init__(influxdb_client=influxdb_client, query_options=query_options)
|
||||
|
||||
def query_csv(self, query: str, org=None, dialect: Dialect = _BaseQueryApi.default_dialect, params: dict = None) \
|
||||
-> CSVIterator:
|
||||
"""
|
||||
Execute the Flux query and return results as a CSV iterator. Each iteration returns a row of the CSV file.
|
||||
|
||||
:param query: a Flux query
|
||||
:param str, Organization org: specifies the organization for executing the query;
|
||||
Take the ``ID``, ``Name`` or ``Organization``.
|
||||
If not specified the default value from ``InfluxDBClient.org`` is used.
|
||||
:param dialect: csv dialect format
|
||||
:param params: bind parameters
|
||||
:return: :class:`~Iterator[List[str]]` wrapped into :class:`~influxdb_client.client.flux_table.CSVIterator`
|
||||
:rtype: CSVIterator
|
||||
|
||||
Serialization the query results to flattened list of values via :func:`~influxdb_client.client.flux_table.CSVIterator.to_values`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from influxdb_client import InfluxDBClient
|
||||
|
||||
with InfluxDBClient(url="http://localhost:8086", token="my-token", org="my-org") as client:
|
||||
|
||||
# Query: using CSV iterator
|
||||
csv_iterator = client.query_api().query_csv('from(bucket:"my-bucket") |> range(start: -10m)')
|
||||
|
||||
# Serialize to values
|
||||
output = csv_iterator.to_values()
|
||||
print(output)
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
[
|
||||
['#datatype', 'string', 'long', 'dateTime:RFC3339', 'dateTime:RFC3339', 'dateTime:RFC3339', 'double', 'string', 'string', 'string']
|
||||
['#group', 'false', 'false', 'true', 'true', 'false', 'false', 'true', 'true', 'true']
|
||||
['#default', '_result', '', '', '', '', '', '', '', '']
|
||||
['', 'result', 'table', '_start', '_stop', '_time', '_value', '_field', '_measurement', 'location']
|
||||
['', '', '0', '2022-06-16', '2022-06-16', '2022-06-16', '24.3', 'temperature', 'my_measurement', 'New York']
|
||||
['', '', '1', '2022-06-16', '2022-06-16', '2022-06-16', '25.3', 'temperature', 'my_measurement', 'Prague']
|
||||
...
|
||||
]
|
||||
|
||||
If you would like to turn off `Annotated CSV header's <https://docs.influxdata.com/influxdb/latest/reference/syntax/annotated-csv/>`_ you can use following code:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from influxdb_client import InfluxDBClient, Dialect
|
||||
|
||||
with InfluxDBClient(url="http://localhost:8086", token="my-token", org="my-org") as client:
|
||||
|
||||
# Query: using CSV iterator
|
||||
csv_iterator = client.query_api().query_csv('from(bucket:"my-bucket") |> range(start: -10m)',
|
||||
dialect=Dialect(header=False, annotations=[]))
|
||||
|
||||
for csv_line in csv_iterator:
|
||||
print(csv_line)
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
[
|
||||
['', '_result', '0', '2022-06-16', '2022-06-16', '2022-06-16', '24.3', 'temperature', 'my_measurement', 'New York']
|
||||
['', '_result', '1', '2022-06-16', '2022-06-16', '2022-06-16', '25.3', 'temperature', 'my_measurement', 'Prague']
|
||||
...
|
||||
]
|
||||
""" # noqa: E501
|
||||
org = self._org_param(org)
|
||||
response = self._query_api.post_query(org=org, query=self._create_query(query, dialect, params),
|
||||
async_req=False, _preload_content=False)
|
||||
|
||||
return self._to_csv(response)
|
||||
|
||||
def query_raw(self, query: str, org=None, dialect=_BaseQueryApi.default_dialect, params: dict = None):
|
||||
"""
|
||||
Execute synchronous Flux query and return result as raw unprocessed result as a str.
|
||||
|
||||
:param query: a Flux query
|
||||
:param str, Organization org: specifies the organization for executing the query;
|
||||
Take the ``ID``, ``Name`` or ``Organization``.
|
||||
If not specified the default value from ``InfluxDBClient.org`` is used.
|
||||
:param dialect: csv dialect format
|
||||
:param params: bind parameters
|
||||
:return: str
|
||||
"""
|
||||
org = self._org_param(org)
|
||||
result = self._query_api.post_query(org=org, query=self._create_query(query, dialect, params), async_req=False,
|
||||
_preload_content=False)
|
||||
|
||||
return result
|
||||
|
||||
def query(self, query: str, org=None, params: dict = None) -> TableList:
|
||||
"""Execute synchronous Flux query and return result as a :class:`~influxdb_client.client.flux_table.FluxTable` list.
|
||||
|
||||
:param query: the Flux query
|
||||
:param str, Organization org: specifies the organization for executing the query;
|
||||
Take the ``ID``, ``Name`` or ``Organization``.
|
||||
If not specified the default value from ``InfluxDBClient.org`` is used.
|
||||
:param params: bind parameters
|
||||
:return: :class:`~influxdb_client.client.flux_table.FluxTable` list wrapped into
|
||||
:class:`~influxdb_client.client.flux_table.TableList`
|
||||
:rtype: TableList
|
||||
|
||||
Serialization the query results to flattened list of values via :func:`~influxdb_client.client.flux_table.TableList.to_values`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from influxdb_client import InfluxDBClient
|
||||
|
||||
with InfluxDBClient(url="http://localhost:8086", token="my-token", org="my-org") as client:
|
||||
|
||||
# Query: using Table structure
|
||||
tables = client.query_api().query('from(bucket:"my-bucket") |> range(start: -10m)')
|
||||
|
||||
# Serialize to values
|
||||
output = tables.to_values(columns=['location', '_time', '_value'])
|
||||
print(output)
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
[
|
||||
['New York', datetime.datetime(2022, 6, 7, 11, 3, 22, 917593, tzinfo=tzutc()), 24.3],
|
||||
['Prague', datetime.datetime(2022, 6, 7, 11, 3, 22, 917593, tzinfo=tzutc()), 25.3],
|
||||
...
|
||||
]
|
||||
|
||||
Serialization the query results to JSON via :func:`~influxdb_client.client.flux_table.TableList.to_json`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from influxdb_client import InfluxDBClient
|
||||
|
||||
with InfluxDBClient(url="http://localhost:8086", token="my-token", org="my-org") as client:
|
||||
|
||||
# Query: using Table structure
|
||||
tables = client.query_api().query('from(bucket:"my-bucket") |> range(start: -10m)')
|
||||
|
||||
# Serialize to JSON
|
||||
output = tables.to_json(indent=5)
|
||||
print(output)
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
[
|
||||
{
|
||||
"_measurement": "mem",
|
||||
"_start": "2021-06-23T06:50:11.897825+00:00",
|
||||
"_stop": "2021-06-25T06:50:11.897825+00:00",
|
||||
"_time": "2020-02-27T16:20:00.897825+00:00",
|
||||
"region": "north",
|
||||
"_field": "usage",
|
||||
"_value": 15
|
||||
},
|
||||
{
|
||||
"_measurement": "mem",
|
||||
"_start": "2021-06-23T06:50:11.897825+00:00",
|
||||
"_stop": "2021-06-25T06:50:11.897825+00:00",
|
||||
"_time": "2020-02-27T16:20:01.897825+00:00",
|
||||
"region": "west",
|
||||
"_field": "usage",
|
||||
"_value": 10
|
||||
},
|
||||
...
|
||||
]
|
||||
""" # noqa: E501
|
||||
org = self._org_param(org)
|
||||
|
||||
response = self._query_api.post_query(org=org, query=self._create_query(query, self.default_dialect, params),
|
||||
async_req=False, _preload_content=False, _return_http_data_only=False)
|
||||
|
||||
return self._to_tables(response, query_options=self._get_query_options())
|
||||
|
||||
def query_stream(self, query: str, org=None, params: dict = None) -> Generator['FluxRecord', Any, None]:
|
||||
"""
|
||||
Execute synchronous Flux query and return stream of FluxRecord as a Generator['FluxRecord'].
|
||||
|
||||
:param query: the Flux query
|
||||
:param str, Organization org: specifies the organization for executing the query;
|
||||
Take the ``ID``, ``Name`` or ``Organization``.
|
||||
If not specified the default value from ``InfluxDBClient.org`` is used.
|
||||
:param params: bind parameters
|
||||
:return: Generator['FluxRecord']
|
||||
"""
|
||||
org = self._org_param(org)
|
||||
|
||||
response = self._query_api.post_query(org=org, query=self._create_query(query, self.default_dialect, params),
|
||||
async_req=False, _preload_content=False, _return_http_data_only=False)
|
||||
return self._to_flux_record_stream(response, query_options=self._get_query_options())
|
||||
|
||||
def query_data_frame(self, query: str, org=None, data_frame_index: List[str] = None, params: dict = None,
|
||||
use_extension_dtypes: bool = False):
|
||||
"""
|
||||
Execute synchronous Flux query and return Pandas DataFrame.
|
||||
|
||||
.. note:: If the ``query`` returns tables with differing schemas than the client generates a :class:`~DataFrame` for each of them.
|
||||
|
||||
:param query: the Flux query
|
||||
:param str, Organization org: specifies the organization for executing the query;
|
||||
Take the ``ID``, ``Name`` or ``Organization``.
|
||||
If not specified the default value from ``InfluxDBClient.org`` is used.
|
||||
:param data_frame_index: the list of columns that are used as DataFrame index
|
||||
:param params: bind parameters
|
||||
:param use_extension_dtypes: set to ``True`` to use panda's extension data types.
|
||||
Useful for queries with ``pivot`` function.
|
||||
When data has missing values, column data type may change (to ``object`` or ``float64``).
|
||||
Nullable extension types (``Int64``, ``Float64``, ``boolean``) support ``panda.NA`` value.
|
||||
For more info, see https://pandas.pydata.org/docs/user_guide/missing_data.html.
|
||||
:return: :class:`~DataFrame` or :class:`~List[DataFrame]`
|
||||
|
||||
.. warning:: For the optimal processing of the query results use the ``pivot() function`` which align results as a table.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
from(bucket:"my-bucket")
|
||||
|> range(start: -5m, stop: now())
|
||||
|> filter(fn: (r) => r._measurement == "mem")
|
||||
|> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
|
||||
|
||||
For more info see:
|
||||
- https://docs.influxdata.com/resources/videos/pivots-in-flux/
|
||||
- https://docs.influxdata.com/flux/latest/stdlib/universe/pivot/
|
||||
- https://docs.influxdata.com/flux/latest/stdlib/influxdata/influxdb/schema/fieldsascols/
|
||||
""" # noqa: E501
|
||||
_generator = self.query_data_frame_stream(query, org=org, data_frame_index=data_frame_index, params=params,
|
||||
use_extension_dtypes=use_extension_dtypes)
|
||||
return self._to_data_frames(_generator)
|
||||
|
||||
def query_data_frame_stream(self, query: str, org=None, data_frame_index: List[str] = None, params: dict = None,
|
||||
use_extension_dtypes: bool = False):
|
||||
"""
|
||||
Execute synchronous Flux query and return stream of Pandas DataFrame as a :class:`~Generator[DataFrame]`.
|
||||
|
||||
.. note:: If the ``query`` returns tables with differing schemas than the client generates a :class:`~DataFrame` for each of them.
|
||||
|
||||
:param query: the Flux query
|
||||
:param str, Organization org: specifies the organization for executing the query;
|
||||
Take the ``ID``, ``Name`` or ``Organization``.
|
||||
If not specified the default value from ``InfluxDBClient.org`` is used.
|
||||
:param data_frame_index: the list of columns that are used as DataFrame index
|
||||
:param params: bind parameters
|
||||
:param use_extension_dtypes: set to ``True`` to use panda's extension data types.
|
||||
Useful for queries with ``pivot`` function.
|
||||
When data has missing values, column data type may change (to ``object`` or ``float64``).
|
||||
Nullable extension types (``Int64``, ``Float64``, ``boolean``) support ``panda.NA`` value.
|
||||
For more info, see https://pandas.pydata.org/docs/user_guide/missing_data.html.
|
||||
:return: :class:`~Generator[DataFrame]`
|
||||
|
||||
.. warning:: For the optimal processing of the query results use the ``pivot() function`` which align results as a table.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
from(bucket:"my-bucket")
|
||||
|> range(start: -5m, stop: now())
|
||||
|> filter(fn: (r) => r._measurement == "mem")
|
||||
|> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
|
||||
|
||||
For more info see:
|
||||
- https://docs.influxdata.com/resources/videos/pivots-in-flux/
|
||||
- https://docs.influxdata.com/flux/latest/stdlib/universe/pivot/
|
||||
- https://docs.influxdata.com/flux/latest/stdlib/influxdata/influxdb/schema/fieldsascols/
|
||||
""" # noqa: E501
|
||||
org = self._org_param(org)
|
||||
|
||||
response = self._query_api.post_query(org=org, query=self._create_query(query, self.default_dialect, params,
|
||||
dataframe_query=True),
|
||||
async_req=False, _preload_content=False, _return_http_data_only=False)
|
||||
|
||||
return self._to_data_frame_stream(data_frame_index=data_frame_index,
|
||||
response=response,
|
||||
query_options=self._get_query_options(),
|
||||
use_extension_dtypes=use_extension_dtypes)
|
||||
|
||||
def __del__(self):
|
||||
"""Close QueryAPI."""
|
||||
pass
|
||||