September 27th, 2025 Update

This commit is contained in:
James
2025-09-27 12:00:00 -07:00
parent 73821d9482
commit a759abb082
1166 changed files with 228204 additions and 267090 deletions
+9 -7
View File
@@ -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")
+335 -63
View File
@@ -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}")
Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 KiB

+1
View File
@@ -0,0 +1 @@
../../../selfdrive/assets/images
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 957 B

+1
View File
@@ -0,0 +1 @@
../../../selfdrive/assets/sounds
Binary file not shown.

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

+498 -364
View File
@@ -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.")
+4 -2
View File
@@ -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
+24 -27
View File
@@ -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):
+59 -23
View File
@@ -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
+125 -151
View File
@@ -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"))
+2 -2
View File
@@ -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)
+16 -14
View File
@@ -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
+94 -124
View File
@@ -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)
+138 -42
View File
@@ -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))
+22 -39
View File
@@ -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
+28 -41
View File
@@ -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()
Binary file not shown.
+18 -30
View File
@@ -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))
+18 -26
View File
@@ -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("Couldnt 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);
}
}
File diff suppressed because it is too large Load Diff
@@ -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">
+708 -10
View File
@@ -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)
+390 -23
View File
@@ -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"
+4
View File
@@ -0,0 +1,4 @@
from .core import contents, where
__all__ = ["contents", "where"]
__version__ = "2025.08.03"
+12
View File
@@ -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())
File diff suppressed because it is too large Load Diff
+83
View File
@@ -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")
View File
+24
View File
@@ -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__
+43
View File
@@ -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
+4
View File
@@ -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)
+89
View File
@@ -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))
+61
View File
@@ -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)
File diff suppressed because it is too large Load Diff
+416
View File
@@ -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
+599
View File
@@ -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
File diff suppressed because it is too large Load Diff
+12
View File
@@ -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."""
+419
View File
@@ -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__
+80
View File
@@ -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
File diff suppressed because it is too large Load Diff
+370
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
# tzwin has moved to dateutil.tz.win
from .tz.win import *
+71
View File
@@ -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
+167
View File
@@ -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
Binary file not shown.
+75
View File
@@ -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?")
+396
View File
@@ -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
+309
View File
@@ -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
+355
View File
@@ -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
+556
View File
@@ -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
+66
View File
@@ -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 tables 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 methods 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 InfluxDatas 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

Some files were not shown because too many files have changed in this diff Show More