mirror of
https://github.com/ivuorinen/aeonview.git
synced 2026-01-26 11:44:03 +00:00
chore: saving the wip state
This commit is contained in:
@@ -6,7 +6,7 @@ root = true
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
max_line_length = 100
|
||||
max_line_length = 80
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
repos:
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.19.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py3-plus]
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: "v0.11.4"
|
||||
rev: "v0.11.5"
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: ["--fix"]
|
||||
|
||||
319
aeonview.py
319
aeonview.py
@@ -13,14 +13,14 @@ import requests
|
||||
# Define messages for logging
|
||||
# These messages are used for logging purposes and can be customized as needed.
|
||||
class AeonViewMessages:
|
||||
INVALID_URL = 'Invalid URL provided.'
|
||||
INVALID_DATE = 'Invalid date format provided.'
|
||||
DOWNLOAD_SUCCESS = 'Image downloaded successfully.'
|
||||
DOWNLOAD_FAILURE = 'Failed to download image.'
|
||||
VIDEO_GENERATION_SUCCESS = 'Video generated successfully.'
|
||||
VIDEO_GENERATION_FAILURE = 'Failed to generate video.'
|
||||
INVALID_IMAGE_FORMAT = 'Invalid image format provided.'
|
||||
INVALID_IMAGE_EXTENSION = 'Invalid image extension provided.'
|
||||
INVALID_URL = "Invalid URL provided."
|
||||
INVALID_DATE = "Invalid date format provided."
|
||||
DOWNLOAD_SUCCESS = "Image downloaded successfully."
|
||||
DOWNLOAD_FAILURE = "Failed to download image."
|
||||
VIDEO_GENERATION_SUCCESS = "Video generated successfully."
|
||||
VIDEO_GENERATION_FAILURE = "Failed to generate video."
|
||||
INVALID_IMAGE_FORMAT = "Invalid image format provided."
|
||||
INVALID_IMAGE_EXTENSION = "Invalid image extension provided."
|
||||
|
||||
|
||||
class AeonViewImages:
|
||||
@@ -58,53 +58,56 @@ class AeonViewImages:
|
||||
if not isinstance(date, datetime):
|
||||
logging.error(AeonViewMessages.INVALID_DATE)
|
||||
sys.exit(1)
|
||||
if not url.startswith('http'):
|
||||
if not url.startswith("http"):
|
||||
logging.error(AeonViewMessages.INVALID_URL)
|
||||
sys.exit(1)
|
||||
if not url.endswith(('.jpg', '.jpeg', '.png', '.gif', '.webp')):
|
||||
if not url.endswith((".jpg", ".jpeg", ".png", ".gif", ".webp")):
|
||||
logging.error(AeonViewMessages.INVALID_IMAGE_FORMAT)
|
||||
sys.exit(1)
|
||||
|
||||
if destination_base is None:
|
||||
logging.error('No destination base path provided.')
|
||||
logging.error("No destination base path provided.")
|
||||
sys.exit(1)
|
||||
if not isinstance(destination_base, Path):
|
||||
logging.error('Invalid destination base path.')
|
||||
logging.error("Invalid destination base path.")
|
||||
sys.exit(1)
|
||||
|
||||
year = date.strftime('%Y')
|
||||
month = date.strftime('%m')
|
||||
day = date.strftime('%d')
|
||||
year = date.strftime("%Y")
|
||||
month = date.strftime("%m")
|
||||
day = date.strftime("%d")
|
||||
|
||||
year_month = f'{year}-{month}'
|
||||
year_month = f"{year}-{month}"
|
||||
|
||||
destination = AeonViewHelpers.build_path(destination_base, year_month, day)
|
||||
file_name = date.strftime('%H-%M-%S') + AeonViewHelpers.get_extension(url)
|
||||
destination = AeonViewHelpers.build_path(
|
||||
destination_base, year_month, day
|
||||
)
|
||||
extension = AeonViewHelpers.get_extension(url) or ""
|
||||
file_name = date.strftime("%H-%M-%S") + extension
|
||||
destination_file = AeonViewHelpers.build_path(destination, file_name)
|
||||
|
||||
if not destination.exists():
|
||||
if self.args.simulate:
|
||||
logging.info(f'Simulate: would create {destination}')
|
||||
if self.args.get("simulate", False):
|
||||
logging.info("Simulate: would create %s", destination)
|
||||
else:
|
||||
AeonViewHelpers.mkdir_p(destination)
|
||||
logging.info(f'Creating destination base path: {destination}')
|
||||
logging.info("Creating destination base path: %s", destination)
|
||||
|
||||
return {
|
||||
'url': url,
|
||||
'file': file_name,
|
||||
'date': {
|
||||
'year': year,
|
||||
'month': month,
|
||||
'day': day,
|
||||
'hour': date.strftime('%H'),
|
||||
'minute': date.strftime('%M'),
|
||||
'second': date.strftime('%S'),
|
||||
"url": url,
|
||||
"file": file_name,
|
||||
"date": {
|
||||
"year": year,
|
||||
"month": month,
|
||||
"day": day,
|
||||
"hour": date.strftime("%H"),
|
||||
"minute": date.strftime("%M"),
|
||||
"second": date.strftime("%S"),
|
||||
},
|
||||
'destinations': {
|
||||
'base': destination_base,
|
||||
'year_month': year_month,
|
||||
'day': day,
|
||||
'file': destination_file,
|
||||
"destinations": {
|
||||
"base": destination_base,
|
||||
"year_month": year_month,
|
||||
"day": day,
|
||||
"file": destination_file,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -113,17 +116,19 @@ class AeonViewImages:
|
||||
Download the image from the URL and save it to the project directory.
|
||||
"""
|
||||
|
||||
if self.args.date is not None:
|
||||
date_param = self.args.get("date", None)
|
||||
|
||||
if date_param is not None:
|
||||
try:
|
||||
date = datetime.strptime(self.args.date, '%Y-%m-%d %H:%M:%S')
|
||||
date = datetime.strptime(date_param, "%Y-%m-%d %H:%M:%S")
|
||||
except ValueError:
|
||||
logging.error(AeonViewMessages.INVALID_DATE)
|
||||
sys.exit(1)
|
||||
else:
|
||||
date = datetime.now()
|
||||
|
||||
img_path = date.strftime('img/%Y-%m/%d')
|
||||
img_name = date.strftime('%H-%M-%S')
|
||||
img_path = date.strftime("img/%Y-%m/%d")
|
||||
img_name = date.strftime("%H-%M-%S")
|
||||
|
||||
if self.url is None:
|
||||
logging.error(AeonViewMessages.INVALID_URL)
|
||||
@@ -132,16 +137,20 @@ class AeonViewImages:
|
||||
file_ext = AeonViewHelpers.get_extension(self.url)
|
||||
|
||||
dest_dir = AeonViewHelpers.build_path(self.project_path, img_path)
|
||||
dest_file = AeonViewHelpers.build_path(dest_dir, f'{img_name}{file_ext}')
|
||||
dest_file = AeonViewHelpers.build_path(
|
||||
dest_dir, f"{img_name}{file_ext}"
|
||||
)
|
||||
|
||||
logging.info(f'Saving image to {dest_file}')
|
||||
logging.info("Saving image to %s", dest_file)
|
||||
|
||||
if not self.args.simulate:
|
||||
if not self.args.get("simulate", False):
|
||||
AeonViewHelpers.mkdir_p(dest_dir)
|
||||
self.download_image(dest_file)
|
||||
else:
|
||||
logging.info(f'Simulate: would create {dest_dir}')
|
||||
logging.info(f'Simulate: would download {self.url} to {dest_file}')
|
||||
logging.info("Simulate: would create %s", dest_dir)
|
||||
logging.info(
|
||||
"Simulate: would download %s to %s", self.url, dest_file
|
||||
)
|
||||
|
||||
def download_image(self, destination: Path):
|
||||
"""
|
||||
@@ -155,22 +164,31 @@ class AeonViewImages:
|
||||
sys.exit(1)
|
||||
|
||||
if not isinstance(destination, Path):
|
||||
logging.error('Invalid destination path.')
|
||||
logging.error("Invalid destination path.")
|
||||
sys.exit(1)
|
||||
|
||||
if self.args.simulate is False or self.args.simulate is None:
|
||||
logging.info(f'Downloading image from {self.url}')
|
||||
response = requests.get(self.url, stream=True)
|
||||
if (
|
||||
self.args.get("simulate", False) is False
|
||||
or self.args.get("simulate", None) is None
|
||||
):
|
||||
logging.info("Downloading image from %s", self.url)
|
||||
response = requests.get(self.url, stream=True, timeout=10)
|
||||
if response.status_code == 200:
|
||||
with open(destination, 'wb') as f:
|
||||
with open(destination, "wb") as f:
|
||||
for chunk in response.iter_content(1024):
|
||||
f.write(chunk)
|
||||
logging.info(f'{AeonViewMessages.DOWNLOAD_SUCCESS}: {destination}')
|
||||
logging.info(
|
||||
"%s: %s", AeonViewMessages.DOWNLOAD_SUCCESS, destination
|
||||
)
|
||||
else:
|
||||
logging.error(f'{AeonViewMessages.DOWNLOAD_FAILURE}: {self.url}')
|
||||
logging.error(
|
||||
"%s: %s", AeonViewMessages.DOWNLOAD_FAILURE, self.url
|
||||
)
|
||||
sys.exit(1)
|
||||
else:
|
||||
logging.info(f'Simulate: would download {self.url} to {destination}')
|
||||
logging.info(
|
||||
"Simulate: would download %s to %s", self.url, destination
|
||||
)
|
||||
|
||||
|
||||
class AeonViewVideos:
|
||||
@@ -181,48 +199,58 @@ class AeonViewVideos:
|
||||
It uses ffmpeg for video processing.
|
||||
"""
|
||||
|
||||
def __init__(self, project_path: Path, args=None):
|
||||
def __init__(
|
||||
self, project_path: Path, args: argparse.Namespace | None = None
|
||||
):
|
||||
"""
|
||||
Initialize the AeonViewVideos class.
|
||||
:param project_path: Path to the project directory
|
||||
:param args: Command line arguments passed to the class
|
||||
"""
|
||||
self.project_path = project_path
|
||||
self.args = args or {}
|
||||
self.args = args
|
||||
|
||||
self.args.simulate = args.simulate or False
|
||||
self.args.fps = args.fps or 10
|
||||
self.args["simulate"] = (
|
||||
args.get("simulate", False) if isinstance(args, dict) else False
|
||||
)
|
||||
self.args["fps"] = args.get("fps", 10) if isinstance(args, dict) else 10
|
||||
|
||||
self.day = args.day or None
|
||||
self.month = args.month or None
|
||||
self.year = args.year or None
|
||||
self.day = args.get("day", None) if args else None
|
||||
self.month = args.get("month", None) if args else None
|
||||
self.year = args.get("year", None) if args else None
|
||||
|
||||
self.path_images = AeonViewHelpers.build_path(self.project_path, 'img')
|
||||
self.path_videos = AeonViewHelpers.build_path(self.project_path, 'vid')
|
||||
self.path_images = AeonViewHelpers.build_path(self.project_path, "img")
|
||||
self.path_videos = AeonViewHelpers.build_path(self.project_path, "vid")
|
||||
|
||||
def generate_daily_video(self):
|
||||
"""
|
||||
Generate a daily video from images.
|
||||
"""
|
||||
|
||||
year_month = f'{self.year}-{self.month}'
|
||||
year_month = f"{self.year}-{self.month}"
|
||||
|
||||
input_dir = AeonViewHelpers.build_path(self.path_videos, year_month, self.day)
|
||||
input_dir = AeonViewHelpers.build_path(
|
||||
self.path_videos, year_month, self.day
|
||||
)
|
||||
output_dir = AeonViewHelpers.build_path(self.path_videos, year_month)
|
||||
output_file = AeonViewHelpers.build_path(output_dir, f'{self.day}.mp4')
|
||||
ffmpeg_cmd = AeonViewHelpers.generate_ffmpeg_command(input_dir, output_file, self.args.fps)
|
||||
output_file = AeonViewHelpers.build_path(output_dir, f"{self.day}.mp4")
|
||||
ffmpeg_cmd = AeonViewHelpers.generate_ffmpeg_command(
|
||||
input_dir, output_file, self.args.fps
|
||||
)
|
||||
|
||||
logging.info(f'Generating video from {input_dir}')
|
||||
logging.info(f'Output file will be {output_file}')
|
||||
logging.info("Generating video from %s", input_dir)
|
||||
logging.info("Output file will be %s", output_file)
|
||||
|
||||
if not self.args.simulate:
|
||||
logging.info(f'Running ffmpeg command: {" ".join(ffmpeg_cmd)}')
|
||||
if not self.args.get("simulate", False):
|
||||
logging.info("Running ffmpeg command: %s", " ".join(ffmpeg_cmd))
|
||||
if not os.path.exists(input_dir):
|
||||
AeonViewHelpers.mkdir_p(output_dir)
|
||||
subprocess.run(ffmpeg_cmd, check=True)
|
||||
logging.info(f'{AeonViewMessages.VIDEO_GENERATION_SUCCESS}: {output_file}')
|
||||
logging.info(
|
||||
"%s: %s", AeonViewMessages.VIDEO_GENERATION_SUCCESS, output_file
|
||||
)
|
||||
else:
|
||||
logging.info(f'Simulate: would run {" ".join(ffmpeg_cmd)}')
|
||||
logging.info("Simulate: would run %s", " ".join(ffmpeg_cmd))
|
||||
|
||||
def generate_monthly_video(self, output_dir: Path):
|
||||
"""
|
||||
@@ -230,7 +258,9 @@ class AeonViewVideos:
|
||||
:param output_dir: Directory where the video will be saved
|
||||
:return: None
|
||||
"""
|
||||
raise NotImplementedError('Monthly video generation is not implemented.')
|
||||
raise NotImplementedError(
|
||||
"Monthly video generation is not implemented."
|
||||
)
|
||||
|
||||
def generate_yearly_video(self, output_dir: Path):
|
||||
"""
|
||||
@@ -238,7 +268,7 @@ class AeonViewVideos:
|
||||
:param output_dir: Directory where the video will be saved
|
||||
:return: None
|
||||
"""
|
||||
raise NotImplementedError('Yearly video generation is not implemented.')
|
||||
raise NotImplementedError("Yearly video generation is not implemented.")
|
||||
|
||||
|
||||
class AeonViewHelpers:
|
||||
@@ -284,20 +314,45 @@ class AeonViewHelpers:
|
||||
def parse_arguments():
|
||||
"""Parse command line arguments."""
|
||||
|
||||
dest_default = str(Path.cwd() / 'projects')
|
||||
dest_default = str(Path.cwd() / "projects")
|
||||
|
||||
parser = argparse.ArgumentParser(description='aeonview - timelapse generator using ffmpeg')
|
||||
parser.add_argument('--mode', choices=['image', 'video'], default='image', help='Run mode')
|
||||
parser.add_argument('--project', help='Project name', default='default')
|
||||
parser.add_argument('--dest', default=dest_default, help='Destination root path')
|
||||
parser.add_argument('--url', help='Webcam URL (required in image mode)')
|
||||
parser.add_argument('--fps', type=int, default=10, help='Frames per second')
|
||||
parser.add_argument('--generate', help='Date for video generation (YYYY-MM-DD)')
|
||||
parser.add_argument('--timeframe', choices=['daily', 'monthly', 'yearly'], default='daily')
|
||||
parser.add_argument(
|
||||
'--simulate', action='store_true', help='Simulation mode', default=False
|
||||
parser = argparse.ArgumentParser(
|
||||
description="aeonview - timelapse generator using ffmpeg"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mode",
|
||||
choices=["image", "video"],
|
||||
default="image",
|
||||
help="Run mode",
|
||||
)
|
||||
parser.add_argument("--project", help="Project name", default="default")
|
||||
parser.add_argument(
|
||||
"--dest", default=dest_default, help="Destination root path"
|
||||
)
|
||||
parser.add_argument("--url", help="Webcam URL (required in image mode)")
|
||||
parser.add_argument(
|
||||
"--fps", type=int, default=10, help="Frames per second"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--generate", help="Date for video generation (YYYY-MM-DD)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--timeframe",
|
||||
choices=["daily", "monthly", "yearly"],
|
||||
default="daily",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--simulate",
|
||||
action="store_true",
|
||||
help="Simulation mode",
|
||||
default=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--verbose",
|
||||
action="store_true",
|
||||
help="Verbose output",
|
||||
default=False,
|
||||
)
|
||||
parser.add_argument('--verbose', action='store_true', help='Verbose output', default=False)
|
||||
args = parser.parse_args()
|
||||
return args, parser
|
||||
|
||||
@@ -311,14 +366,14 @@ class AeonViewHelpers:
|
||||
logging.error(AeonViewMessages.INVALID_IMAGE_EXTENSION)
|
||||
return None
|
||||
|
||||
if url.endswith('.png'):
|
||||
return '.png'
|
||||
elif url.endswith('.gif'):
|
||||
return '.gif'
|
||||
elif url.endswith('.webp'):
|
||||
return '.webp'
|
||||
if url.endswith(".png"):
|
||||
return ".png"
|
||||
elif url.endswith(".gif"):
|
||||
return ".gif"
|
||||
elif url.endswith(".webp"):
|
||||
return ".webp"
|
||||
else:
|
||||
return '.jpg'
|
||||
return ".jpg"
|
||||
|
||||
@staticmethod
|
||||
def setup_logger(verbose: bool):
|
||||
@@ -328,10 +383,12 @@ class AeonViewHelpers:
|
||||
:return: None
|
||||
"""
|
||||
level = logging.DEBUG if verbose else logging.INFO
|
||||
logging.basicConfig(level=level, format='[%(levelname)s] %(message)s')
|
||||
logging.basicConfig(level=level, format="[%(levelname)s] %(message)s")
|
||||
|
||||
@staticmethod
|
||||
def generate_ffmpeg_command(input_dir: Path, output_file: Path, fps: int = 10) -> list:
|
||||
def generate_ffmpeg_command(
|
||||
input_dir: Path, output_file: Path, fps: int = 10
|
||||
) -> list:
|
||||
"""
|
||||
Generate the ffmpeg command to create a video from images.
|
||||
:param input_dir: Directory containing the images
|
||||
@@ -340,17 +397,17 @@ class AeonViewHelpers:
|
||||
:return: Newly created ffmpeg command as a list
|
||||
"""
|
||||
return [
|
||||
'ffmpeg',
|
||||
'-framerate',
|
||||
"ffmpeg",
|
||||
"-framerate",
|
||||
str(fps),
|
||||
'-pattern_type',
|
||||
'glob',
|
||||
'-i',
|
||||
str(input_dir / '*.{jpg,jpeg,png,gif,webp}'),
|
||||
'-c:v',
|
||||
'libx264',
|
||||
'-pix_fmt',
|
||||
'yuv420p',
|
||||
"-pattern_type",
|
||||
"glob",
|
||||
"-i",
|
||||
str(input_dir / "*.{jpg,jpeg,png,gif,webp}"),
|
||||
"-c:v",
|
||||
"libx264",
|
||||
"-pix_fmt",
|
||||
"yuv420p",
|
||||
str(output_file),
|
||||
]
|
||||
|
||||
@@ -370,11 +427,11 @@ class AeonViewApp:
|
||||
Execute the application based on the provided arguments.
|
||||
"""
|
||||
if self.args.simulate:
|
||||
logging.info('Simulation mode active. No actions will be executed.')
|
||||
logging.info("Simulation mode active. No actions will be executed.")
|
||||
|
||||
if self.args.mode == 'image':
|
||||
if self.args.mode == "image":
|
||||
self.process_image()
|
||||
elif self.args.mode == 'video':
|
||||
elif self.args.mode == "video":
|
||||
self.process_video()
|
||||
|
||||
def process_image(self):
|
||||
@@ -382,18 +439,20 @@ class AeonViewApp:
|
||||
Process image download and saving based on the provided arguments.
|
||||
"""
|
||||
if not self.args.url or self.args.url is None:
|
||||
logging.error('--url is required in image mode')
|
||||
logging.error("--url is required in image mode")
|
||||
self.parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
if not isinstance(self.args.url, str) or not self.args.url.startswith('http'):
|
||||
logging.error(f'{AeonViewMessages.INVALID_URL}: {self.args.url}')
|
||||
if not isinstance(self.args.url, str) or not self.args.url.startswith(
|
||||
"http"
|
||||
):
|
||||
logging.error("%s: %s", AeonViewMessages.INVALID_URL, self.args.url)
|
||||
sys.exit(1)
|
||||
|
||||
url = self.args.url
|
||||
project = self.args.project or 'default'
|
||||
project = self.args.project or "default"
|
||||
|
||||
if project == 'default' and url:
|
||||
if project == "default" and url:
|
||||
project = hashlib.md5(url.encode()).hexdigest()[:5]
|
||||
|
||||
project_path = AeonViewHelpers.build_path(self.base_path, project)
|
||||
@@ -405,23 +464,25 @@ class AeonViewApp:
|
||||
Process video generation based on the provided arguments.
|
||||
"""
|
||||
if not self.args.project:
|
||||
logging.error('--project is required in video mode')
|
||||
logging.error("--project is required in video mode")
|
||||
self.parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
if not isinstance(self.args.project, str):
|
||||
logging.error(f'Invalid project name: {self.args.project}')
|
||||
logging.error("Invalid project name: %s", self.args.project)
|
||||
sys.exit(1)
|
||||
|
||||
project_path = AeonViewHelpers.build_path(self.base_path, self.args.project)
|
||||
project_path = AeonViewHelpers.build_path(
|
||||
self.base_path, self.args.project
|
||||
)
|
||||
|
||||
if not os.path.exists(project_path):
|
||||
logging.error(f'Project path {project_path} does not exist.')
|
||||
logging.error("Project path %s does not exist.", project_path)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
generate_date = (
|
||||
datetime.strptime(self.args.generate, '%Y-%m-%d')
|
||||
datetime.strptime(self.args.generate, "%Y-%m-%d")
|
||||
if self.args.generate
|
||||
else datetime.today() - timedelta(days=1)
|
||||
)
|
||||
@@ -429,31 +490,33 @@ class AeonViewApp:
|
||||
logging.error(AeonViewMessages.INVALID_DATE)
|
||||
sys.exit(1)
|
||||
|
||||
year = generate_date.strftime('%Y')
|
||||
month = generate_date.strftime('%m')
|
||||
day = generate_date.strftime('%d')
|
||||
year = generate_date.strftime("%Y")
|
||||
month = generate_date.strftime("%m")
|
||||
day = generate_date.strftime("%d")
|
||||
|
||||
self.args.day = day
|
||||
self.args.month = month
|
||||
self.args.year = year
|
||||
|
||||
if not AeonViewHelpers.check_date(int(year), int(month), int(day)):
|
||||
logging.error(f'Invalid date: {year}-{month}-{day}')
|
||||
logging.error("Invalid date: %s-%s-%s", year, month, day)
|
||||
sys.exit(1)
|
||||
|
||||
avm = AeonViewVideos(project_path, self.args)
|
||||
avm = AeonViewVideos(project_path, vars(self.args))
|
||||
|
||||
if self.args.timeframe == 'daily':
|
||||
if self.args.timeframe == "daily":
|
||||
avm.generate_daily_video()
|
||||
elif self.args.timeframe == 'monthly':
|
||||
output_dir = AeonViewHelpers.build_path(project_path, 'vid', f'{year}-{month}')
|
||||
elif self.args.timeframe == "monthly":
|
||||
output_dir = AeonViewHelpers.build_path(
|
||||
project_path, "vid", f"{year}-{month}"
|
||||
)
|
||||
avm.generate_monthly_video(output_dir)
|
||||
elif self.args.timeframe == 'yearly':
|
||||
output_dir = AeonViewHelpers.build_path(project_path, 'vid', year)
|
||||
elif self.args.timeframe == "yearly":
|
||||
output_dir = AeonViewHelpers.build_path(project_path, "vid", year)
|
||||
avm.generate_yearly_video(output_dir)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
app = AeonViewApp()
|
||||
app.run()
|
||||
|
||||
|
||||
283
aeonview_test.py
283
aeonview_test.py
@@ -1,24 +1,31 @@
|
||||
# vim: set ft=python ts=4 sw=4 sts=4 et wrap:
|
||||
import argparse
|
||||
import logging
|
||||
import subprocess
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from unittest import TestCase, mock
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from aeonview import AeonViewHelpers, AeonViewImages, AeonViewVideos, AeonViewMessages
|
||||
from aeonview import (
|
||||
AeonViewHelpers,
|
||||
AeonViewImages,
|
||||
AeonViewMessages,
|
||||
AeonViewVideos,
|
||||
)
|
||||
|
||||
# Define values used in the tests
|
||||
default_dest = str(Path.cwd() / 'projects')
|
||||
default_project = 'default'
|
||||
default_dest = str(Path.cwd() / "projects")
|
||||
default_project = "default"
|
||||
default_fps = 10
|
||||
default_timeframe = 'daily'
|
||||
default_timeframe = "daily"
|
||||
default_simulate = False
|
||||
default_verbose = False
|
||||
default_image_domain = 'https://example.com/image'
|
||||
default_test_path = Path('/tmp/test_project').resolve()
|
||||
default_image_domain = "https://example.com/image"
|
||||
default_test_path = Path("/tmp/test_project").resolve()
|
||||
tmp_images = Path("/tmp/images")
|
||||
|
||||
|
||||
# Define the Helpers class with methods to be tested
|
||||
@@ -31,52 +38,61 @@ class TestHelpers(TestCase):
|
||||
|
||||
def test_mkdir_p_creates_directory(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
test_path = Path(tmp) / 'a' / 'b' / 'c'
|
||||
test_path = Path(tmp) / "a" / "b" / "c"
|
||||
AeonViewHelpers.mkdir_p(test_path)
|
||||
self.assertTrue(test_path.exists())
|
||||
self.assertTrue(test_path.is_dir())
|
||||
|
||||
def test_get_extension_valid(self):
|
||||
self.assertEqual(AeonViewHelpers.get_extension(f'{default_image_domain}.png'), '.png')
|
||||
self.assertEqual(AeonViewHelpers.get_extension(f'{default_image_domain}.jpg'), '.jpg')
|
||||
self.assertEqual(AeonViewHelpers.get_extension(f'{default_image_domain}.gif'), '.gif')
|
||||
self.assertEqual(AeonViewHelpers.get_extension(f'{default_image_domain}.webp'), '.webp')
|
||||
prefix = default_image_domain
|
||||
types = ["jpg", "png", "gif", "webp"]
|
||||
for ext in types:
|
||||
self.assertEqual(
|
||||
AeonViewHelpers.get_extension(f"{prefix}.{ext}"),
|
||||
f".{ext}",
|
||||
)
|
||||
|
||||
def test_get_extension_invalid(self):
|
||||
self.assertEqual(
|
||||
AeonViewHelpers.get_extension(default_image_domain), '.jpg'
|
||||
AeonViewHelpers.get_extension(default_image_domain), ".jpg"
|
||||
) # Default behavior
|
||||
self.assertIsNone(AeonViewHelpers.get_extension(None))
|
||||
|
||||
|
||||
class TestFFmpegCommand(TestCase):
|
||||
def test_generate_ffmpeg_command(self):
|
||||
input_dir = Path('/tmp/images')
|
||||
output_file = Path('/tmp/output.mp4')
|
||||
input_dir = tmp_images
|
||||
output_file = Path("/tmp/output.mp4")
|
||||
fps = 24
|
||||
cmd = AeonViewHelpers.generate_ffmpeg_command(input_dir, output_file, fps)
|
||||
self.assertIn('ffmpeg', cmd[0])
|
||||
cmd = AeonViewHelpers.generate_ffmpeg_command(
|
||||
input_dir, output_file, fps
|
||||
)
|
||||
self.assertIn("ffmpeg", cmd[0])
|
||||
self.assertIn(str(fps), cmd)
|
||||
self.assertEqual(str(output_file), cmd[-1])
|
||||
self.assertIn(str(input_dir / '*.{jpg,jpeg,png,gif,webp}'), cmd)
|
||||
self.assertIn(str(input_dir / "*.{jpg,jpeg,png,gif,webp}"), cmd)
|
||||
|
||||
def test_generate_ffmpeg_command_output_format(self):
|
||||
input_dir = Path('/tmp/images')
|
||||
output_file = Path('/tmp/video.mp4')
|
||||
cmd = AeonViewHelpers.generate_ffmpeg_command(input_dir, output_file, 30)
|
||||
self.assertIn('/tmp/images/*.{jpg,jpeg,png,gif,webp}', cmd)
|
||||
self.assertIn('/tmp/video.mp4', cmd)
|
||||
self.assertIn('-c:v', cmd)
|
||||
self.assertIn('libx264', cmd)
|
||||
self.assertIn('-pix_fmt', cmd)
|
||||
self.assertIn('yuv420p', cmd)
|
||||
input_dir = tmp_images
|
||||
output_file = Path("/tmp/video.mp4")
|
||||
cmd = AeonViewHelpers.generate_ffmpeg_command(
|
||||
input_dir, output_file, 30
|
||||
)
|
||||
self.assertIn("/tmp/images/*.{jpg,jpeg,png,gif,webp}", cmd)
|
||||
self.assertIn("/tmp/video.mp4", cmd)
|
||||
self.assertIn("-c:v", cmd)
|
||||
self.assertIn("libx264", cmd)
|
||||
self.assertIn("-pix_fmt", cmd)
|
||||
self.assertIn("yuv420p", cmd)
|
||||
|
||||
@mock.patch('subprocess.run')
|
||||
@mock.patch("subprocess.run")
|
||||
def test_simulate_ffmpeg_call(self, mock_run):
|
||||
input_dir = Path('/tmp/images')
|
||||
output_file = Path('/tmp/out.mp4')
|
||||
cmd = AeonViewHelpers.generate_ffmpeg_command(input_dir, output_file, 10)
|
||||
subprocess.run(cmd)
|
||||
input_dir = tmp_images
|
||||
output_file = Path("/tmp/out.mp4")
|
||||
cmd = AeonViewHelpers.generate_ffmpeg_command(
|
||||
input_dir, output_file, 10
|
||||
)
|
||||
subprocess.run(cmd, check=True)
|
||||
mock_run.assert_called_once_with(cmd)
|
||||
|
||||
|
||||
@@ -84,235 +100,276 @@ class TestAeonViewImages(TestCase):
|
||||
def setUp(self):
|
||||
self.args = argparse.Namespace()
|
||||
self.args.simulate = False
|
||||
self.args.date = '2025-04-10 12:30:45'
|
||||
self.args.url = f'{default_image_domain}.jpg'
|
||||
self.args.date = "2025-04-10 12:30:45"
|
||||
self.args.url = f"{default_image_domain}.jpg"
|
||||
self.args.dest = default_test_path
|
||||
self.args.project = default_project
|
||||
self.args.verbose = default_verbose
|
||||
self.args.fps = default_fps
|
||||
self.args.timeframe = default_timeframe
|
||||
self.project_path = default_test_path
|
||||
self.url = f'{default_image_domain}.jpg'
|
||||
self.url = f"{default_image_domain}.jpg"
|
||||
|
||||
def test_get_image_paths_valid(self):
|
||||
url = f'{default_image_domain}.jpg'
|
||||
url = f"{default_image_domain}.jpg"
|
||||
destination_base = default_test_path
|
||||
date = datetime(2025, 4, 10, 12, 30, 45)
|
||||
paths = AeonViewImages.get_image_paths(self, url, destination_base, date)
|
||||
self.assertEqual(paths['url'], url)
|
||||
self.assertEqual(paths['file'], '12-30-45.jpg')
|
||||
aeon_view_images = AeonViewImages(destination_base, url)
|
||||
paths = aeon_view_images.get_image_paths(url, destination_base, date)
|
||||
self.assertEqual(paths["url"], url)
|
||||
self.assertEqual(paths["file"], "12-30-45.jpg")
|
||||
self.assertEqual(
|
||||
paths['destinations']['file'], destination_base / '2025-04' / '10' / '12-30-45.jpg'
|
||||
paths["destinations"]["file"],
|
||||
destination_base / "2025-04" / "10" / "12-30-45.jpg",
|
||||
)
|
||||
|
||||
def test_get_image_paths_invalid_url(self):
|
||||
with pytest.raises(SystemExit):
|
||||
with self.assertLogs(level='ERROR') as log:
|
||||
AeonViewImages.get_image_paths(
|
||||
self, 'invalid-url', default_test_path, datetime(2025, 4, 10)
|
||||
with self.assertLogs(level="ERROR") as log:
|
||||
aeon_view_images = AeonViewImages(
|
||||
default_test_path, "invalid-url"
|
||||
)
|
||||
aeon_view_images.get_image_paths(
|
||||
"invalid-url",
|
||||
default_test_path,
|
||||
datetime(2025, 4, 10),
|
||||
)
|
||||
self.assertIn(AeonViewMessages.INVALID_URL, log.output[0])
|
||||
|
||||
def test_get_image_paths_invalid_date(self):
|
||||
with pytest.raises(SystemExit):
|
||||
with self.assertLogs(level='ERROR') as log:
|
||||
AeonViewImages.get_image_paths(
|
||||
self, f'{default_image_domain}.jpg', default_test_path, 'invalid-date'
|
||||
with self.assertLogs(level="ERROR") as log:
|
||||
aeon_view_images = AeonViewImages(
|
||||
default_test_path, f"{default_image_domain}.jpg"
|
||||
)
|
||||
aeon_view_images.get_image_paths(
|
||||
f"{default_image_domain}.jpg",
|
||||
default_test_path,
|
||||
"invalid-date",
|
||||
)
|
||||
self.assertIn(AeonViewMessages.INVALID_DATE, log.output[0])
|
||||
|
||||
@mock.patch('aeonview.AeonViewHelpers.mkdir_p')
|
||||
@mock.patch('aeonview.AeonViewImages.download_image')
|
||||
@mock.patch("aeonview.AeonViewHelpers.mkdir_p")
|
||||
@mock.patch("aeonview.AeonViewImages.download_image")
|
||||
def test_get_current_image(self, mock_download_image, mock_mkdir_p):
|
||||
project_path = default_test_path
|
||||
url = f'{default_image_domain}.jpg'
|
||||
args = argparse.Namespace(simulate=False, date='2025-04-10 12:30:45')
|
||||
url = f"{default_image_domain}.jpg"
|
||||
args = argparse.Namespace(simulate=False, date="2025-04-10 12:30:45")
|
||||
avi = AeonViewImages(project_path, url, args)
|
||||
avi.get_current_image()
|
||||
mock_mkdir_p.assert_called()
|
||||
mock_download_image.assert_called()
|
||||
|
||||
@mock.patch('aeonview.requests.get')
|
||||
@mock.patch("aeonview.requests.get")
|
||||
def test_download_image_success(self, mock_get):
|
||||
mock_response = mock.Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.iter_content = mock.Mock(return_value=[b'data'])
|
||||
mock_response.iter_content = mock.Mock(return_value=[b"data"])
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
project_path = default_test_path
|
||||
url = f'{default_image_domain}.jpg'
|
||||
url = f"{default_image_domain}.jpg"
|
||||
args = argparse.Namespace(simulate=False)
|
||||
avi = AeonViewImages(project_path, url, args)
|
||||
destination = Path('/tmp/image.jpg')
|
||||
avi.download_image(destination)
|
||||
with tempfile.NamedTemporaryFile(delete=True) as temp_file:
|
||||
destination = Path(temp_file.name)
|
||||
avi.download_image(destination)
|
||||
|
||||
mock_get.assert_called_once_with(url, stream=True)
|
||||
self.assertTrue(destination.exists())
|
||||
mock_get.assert_called_once_with(url, stream=True)
|
||||
self.assertTrue(destination.exists())
|
||||
|
||||
@mock.patch('aeonview.requests.get')
|
||||
@mock.patch("aeonview.requests.get")
|
||||
def test_download_image_failure(self, mock_get):
|
||||
mock_response = mock.Mock()
|
||||
mock_response.status_code = 404
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
project_path = default_test_path
|
||||
url = f'{default_image_domain}.jpg'
|
||||
url = f"{default_image_domain}.jpg"
|
||||
args = argparse.Namespace(simulate=False)
|
||||
avi = AeonViewImages(project_path, url, args)
|
||||
destination = Path('/tmp/image.jpg')
|
||||
destination = Path("/tmp/image.jpg")
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
with self.assertLogs(level='ERROR') as log:
|
||||
with self.assertLogs(level="ERROR") as log:
|
||||
avi.download_image(destination)
|
||||
self.assertIn(AeonViewMessages.DOWNLOAD_FAILURE, log.output[0])
|
||||
|
||||
|
||||
class TestAeonViewVideos(TestCase):
|
||||
@mock.patch('aeonview.AeonViewHelpers.mkdir_p')
|
||||
@mock.patch('subprocess.run')
|
||||
@mock.patch("aeonview.AeonViewHelpers.mkdir_p")
|
||||
@mock.patch("subprocess.run")
|
||||
def test_generate_daily_video(self, mock_subprocess_run, mock_mkdir_p):
|
||||
project_path = default_test_path
|
||||
args = argparse.Namespace(simulate=False, fps=10, day='01', month='04', year='2025')
|
||||
args = argparse.Namespace(
|
||||
simulate=False, fps=10, day="01", month="04", year="2025"
|
||||
)
|
||||
avv = AeonViewVideos(project_path, args)
|
||||
with self.assertLogs(level='INFO') as log:
|
||||
with self.assertLogs(level="INFO") as log:
|
||||
avv.generate_daily_video()
|
||||
expected_message = f'{AeonViewMessages.VIDEO_GENERATION_SUCCESS}: {default_test_path / "vid/2025-04/01.mp4"}'
|
||||
self.assertIn(expected_message, log.output[-1]) # Ensure it's the last log entry
|
||||
msg = AeonViewMessages.VIDEO_GENERATION_SUCCESS
|
||||
expected_message = (
|
||||
f"{msg}: {default_test_path / 'vid/2025-04/01.mp4'}"
|
||||
)
|
||||
self.assertIn(
|
||||
expected_message, log.output[-1]
|
||||
) # Ensure it's the last log entry
|
||||
mock_mkdir_p.assert_called()
|
||||
mock_subprocess_run.assert_called()
|
||||
|
||||
@mock.patch('aeonview.AeonViewHelpers.mkdir_p')
|
||||
@mock.patch("aeonview.AeonViewHelpers.mkdir_p")
|
||||
def test_generate_daily_video_simulate(self, mock_mkdir_p):
|
||||
project_path = default_test_path
|
||||
args = argparse.Namespace(simulate=True, fps=10, day='01', month='04', year='2025')
|
||||
args = argparse.Namespace(
|
||||
simulate=True, fps=10, day="01", month="04", year="2025"
|
||||
)
|
||||
avv = AeonViewVideos(project_path, args)
|
||||
avv.generate_daily_video()
|
||||
mock_mkdir_p.assert_not_called()
|
||||
|
||||
def test_generate_monthly_video_not_implemented(self):
|
||||
project_path = default_test_path
|
||||
args = argparse.Namespace(simulate=False, fps=10, day='01', month='04', year='2025')
|
||||
args = argparse.Namespace(
|
||||
simulate=False, fps=10, day="01", month="04", year="2025"
|
||||
)
|
||||
avv = AeonViewVideos(project_path, args)
|
||||
with pytest.raises(NotImplementedError):
|
||||
avv.generate_monthly_video(Path('/tmp'))
|
||||
avv.generate_monthly_video(Path("/tmp"))
|
||||
|
||||
def test_generate_yearly_video_not_implemented(self):
|
||||
project_path = default_test_path
|
||||
args = argparse.Namespace(simulate=False, fps=10, day='01', month='04', year='2025')
|
||||
args = argparse.Namespace(
|
||||
simulate=False, fps=10, day="01", month="04", year="2025"
|
||||
)
|
||||
avv = AeonViewVideos(project_path, args)
|
||||
with pytest.raises(NotImplementedError):
|
||||
avv.generate_yearly_video(Path('/tmp'))
|
||||
avv.generate_yearly_video(Path("/tmp"))
|
||||
|
||||
|
||||
class TestHelpersArguments(TestCase):
|
||||
def setUp(self):
|
||||
self.default_dest = str(Path.cwd() / 'projects')
|
||||
self.default_dest = str(Path.cwd() / "projects")
|
||||
|
||||
@mock.patch(
|
||||
'sys.argv', ['aeonview.py', '--mode', 'image', '--url', f'{default_image_domain}.jpg']
|
||||
"sys.argv",
|
||||
[
|
||||
"aeonview.py",
|
||||
"--mode",
|
||||
"image",
|
||||
"--url",
|
||||
f"{default_image_domain}.jpg",
|
||||
],
|
||||
)
|
||||
def test_parse_arguments_image_mode(self):
|
||||
args, _ = AeonViewHelpers.parse_arguments()
|
||||
self.assertEqual(args.mode, 'image')
|
||||
self.assertEqual(args.url, f'{default_image_domain}.jpg')
|
||||
self.assertEqual(args.mode, "image")
|
||||
self.assertEqual(args.url, f"{default_image_domain}.jpg")
|
||||
self.assertEqual(args.dest, self.default_dest)
|
||||
|
||||
@mock.patch('sys.argv', ['aeonview.py', '--mode', 'video', '--project', f'{default_project}'])
|
||||
@mock.patch(
|
||||
"sys.argv",
|
||||
["aeonview.py", "--mode", "video", "--project", f"{default_project}"],
|
||||
)
|
||||
def test_parse_arguments_video_mode(self):
|
||||
args, _ = AeonViewHelpers.parse_arguments()
|
||||
self.assertEqual(args.mode, 'video')
|
||||
self.assertEqual(args.project, f'{default_project}')
|
||||
self.assertEqual(args.mode, "video")
|
||||
self.assertEqual(args.project, f"{default_project}")
|
||||
self.assertEqual(args.dest, self.default_dest)
|
||||
|
||||
@mock.patch('sys.argv', ['aeonview.py', '--mode', 'image', '--simulate'])
|
||||
@mock.patch("sys.argv", ["aeonview.py", "--mode", "image", "--simulate"])
|
||||
def test_parse_arguments_simulate_mode(self):
|
||||
args, _ = AeonViewHelpers.parse_arguments()
|
||||
self.assertEqual(args.mode, 'image')
|
||||
self.assertEqual(args.mode, "image")
|
||||
self.assertTrue(args.simulate)
|
||||
|
||||
@mock.patch('sys.argv', ['aeonview.py', '--mode', 'video', '--fps', '30'])
|
||||
@mock.patch("sys.argv", ["aeonview.py", "--mode", "video", "--fps", "30"])
|
||||
def test_parse_arguments_fps(self):
|
||||
args, _ = AeonViewHelpers.parse_arguments()
|
||||
self.assertEqual(args.mode, 'video')
|
||||
self.assertEqual(args.project, f'{default_project}')
|
||||
self.assertEqual(args.mode, "video")
|
||||
self.assertEqual(args.project, f"{default_project}")
|
||||
self.assertEqual(args.dest, self.default_dest)
|
||||
self.assertEqual(args.fps, 30)
|
||||
|
||||
@mock.patch('sys.argv', ['aeonview.py', '--mode', 'video', '--generate', '2023-10-01'])
|
||||
@mock.patch(
|
||||
"sys.argv",
|
||||
["aeonview.py", "--mode", "video", "--generate", "2023-10-01"],
|
||||
)
|
||||
def test_parse_arguments_generate_date(self):
|
||||
args, _ = AeonViewHelpers.parse_arguments()
|
||||
self.assertEqual(args.mode, 'video')
|
||||
self.assertEqual(args.generate, '2023-10-01')
|
||||
self.assertEqual(args.mode, "video")
|
||||
self.assertEqual(args.generate, "2023-10-01")
|
||||
|
||||
@mock.patch('sys.argv', ['aeonview.py', '--mode', 'image', '--verbose'])
|
||||
@mock.patch("sys.argv", ["aeonview.py", "--mode", "image", "--verbose"])
|
||||
def test_parse_arguments_verbose(self):
|
||||
args, _ = AeonViewHelpers.parse_arguments()
|
||||
self.assertEqual(args.mode, 'image')
|
||||
self.assertEqual(args.mode, "image")
|
||||
self.assertTrue(args.verbose)
|
||||
|
||||
@mock.patch('sys.argv', ['aeonview.py'])
|
||||
@mock.patch("sys.argv", ["aeonview.py"])
|
||||
def test_parse_arguments_defaults(self):
|
||||
args, _ = AeonViewHelpers.parse_arguments()
|
||||
self.assertEqual(args.mode, 'image')
|
||||
self.assertEqual(args.project, f'{default_project}')
|
||||
self.assertEqual(args.mode, "image")
|
||||
self.assertEqual(args.project, f"{default_project}")
|
||||
self.assertEqual(args.dest, self.default_dest)
|
||||
self.assertEqual(args.fps, 10)
|
||||
self.assertEqual(args.timeframe, 'daily')
|
||||
self.assertEqual(args.timeframe, "daily")
|
||||
self.assertFalse(args.simulate)
|
||||
self.assertFalse(args.verbose)
|
||||
|
||||
|
||||
class TestAeonViewSimulation(TestCase):
|
||||
@mock.patch('aeonview.AeonViewHelpers.mkdir_p')
|
||||
@mock.patch('aeonview.AeonViewImages.download_image')
|
||||
@mock.patch("aeonview.AeonViewHelpers.mkdir_p")
|
||||
@mock.patch("aeonview.AeonViewImages.download_image")
|
||||
def test_image_simulation(self, mock_download_image, mock_mkdir_p):
|
||||
args = mock.MagicMock()
|
||||
args.simulate = True
|
||||
args.date = '2025-04-10 12:30:45'
|
||||
args.date = "2025-04-10 12:30:45"
|
||||
|
||||
url = f'{default_image_domain}.jpg'
|
||||
project_path = Path('/tmp/test_project').resolve()
|
||||
url = f"{default_image_domain}.jpg"
|
||||
project_path = Path("/tmp/test_project").resolve()
|
||||
|
||||
avi = AeonViewImages(project_path, url, args)
|
||||
with mock.patch('aeonview.logging.info') as mock_logging:
|
||||
with mock.patch("aeonview.logging.info") as mock_logging:
|
||||
avi.get_current_image()
|
||||
mock_mkdir_p.assert_not_called()
|
||||
mock_download_image.assert_not_called()
|
||||
mock_logging.assert_any_call(
|
||||
f'Saving image to {project_path}/img/2025-04/10/12-30-45.jpg'
|
||||
f"Saving image to {project_path}/img/2025-04/10/12-30-45.jpg"
|
||||
)
|
||||
|
||||
@mock.patch('aeonview.AeonViewHelpers.mkdir_p')
|
||||
@mock.patch('subprocess.run')
|
||||
@mock.patch("aeonview.AeonViewHelpers.mkdir_p")
|
||||
@mock.patch("subprocess.run")
|
||||
def test_video_simulation(self, mock_subprocess_run, mock_mkdir_p):
|
||||
args = mock.MagicMock()
|
||||
args.simulate = True
|
||||
args.fps = 10
|
||||
args.day = '01'
|
||||
args.month = '01'
|
||||
args.year = '2023'
|
||||
project_path = Path('/tmp/test_project').resolve()
|
||||
args.day = "01"
|
||||
args.month = "01"
|
||||
args.year = "2023"
|
||||
project_path = Path("/tmp/test_project").resolve()
|
||||
|
||||
avv = AeonViewVideos(project_path, args)
|
||||
with mock.patch('aeonview.logging.info') as mock_logging:
|
||||
with mock.patch("aeonview.logging.info") as mock_logging:
|
||||
avv.generate_daily_video()
|
||||
mock_mkdir_p.assert_not_called()
|
||||
mock_subprocess_run.assert_not_called()
|
||||
mock_logging.assert_any_call(f'Generating video from {project_path}/vid/2023-01/01')
|
||||
mock_logging.assert_any_call(
|
||||
f"Generating video from {project_path}/vid/2023-01/01"
|
||||
)
|
||||
|
||||
|
||||
class TestSetupLogger(TestCase):
|
||||
@mock.patch('logging.basicConfig')
|
||||
@mock.patch("logging.basicConfig")
|
||||
def test_setup_logger_verbose(self, mock_basic_config):
|
||||
AeonViewHelpers.setup_logger(verbose=True)
|
||||
mock_basic_config.assert_called_once_with(
|
||||
level=logging.DEBUG, format='[%(levelname)s] %(message)s'
|
||||
level=logging.DEBUG, format="[%(levelname)s] %(message)s"
|
||||
)
|
||||
|
||||
@mock.patch('logging.basicConfig')
|
||||
@mock.patch("logging.basicConfig")
|
||||
def test_setup_logger_non_verbose(self, mock_basic_config):
|
||||
AeonViewHelpers.setup_logger(verbose=False)
|
||||
mock_basic_config.assert_called_once_with(
|
||||
level=logging.INFO, format='[%(levelname)s] %(message)s'
|
||||
level=logging.INFO, format="[%(levelname)s] %(message)s"
|
||||
)
|
||||
mock_basic_config.reset_mock()
|
||||
|
||||
@@ -6,4 +6,4 @@ import pytest
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def no_network_requests(monkeypatch):
|
||||
monkeypatch.setattr('subprocess.run', mock.Mock())
|
||||
monkeypatch.setattr("subprocess.run", mock.Mock())
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
line-length = 80
|
||||
target-version = "py311"
|
||||
|
||||
[tool.ruff.lint]
|
||||
@@ -7,10 +7,10 @@ select = ["E", "F", "I", "B", "UP", "C4", "T20"]
|
||||
ignore = ["E501"]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"test_*.py" = ["S101"]
|
||||
"*_test.py" = ["S101"]
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = "single"
|
||||
quote-style = "double"
|
||||
indent-style = "space"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
|
||||
Reference in New Issue
Block a user