Skip to content

Commit

Permalink
Added URL support for template {function}
Browse files Browse the repository at this point in the history
  • Loading branch information
RhetTbull committed Nov 24, 2023
1 parent d216515 commit 79806a8
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 15 deletions.
2 changes: 0 additions & 2 deletions osxphotos/cli/install_uninstall_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@

import click

from osxphotos.utils import is_http_url, download_url_to_dir
from .param_types import PathOrURL
import tempfile


class RunCommand(click.Command):
Expand Down
11 changes: 4 additions & 7 deletions osxphotos/cli/param_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,17 @@
import pytimeparse2
from strpdatetime import strpdatetime

import osxphotos.tempdir as tempdir
from osxphotos.export_db_utils import export_db_get_version
from osxphotos.photoinfo import PhotoInfoNone
from osxphotos.phototemplate import PhotoTemplate, RenderOptions
from osxphotos.timeutils import time_string_to_datetime, utc_offset_string_to_seconds
from osxphotos.timezones import Timezone
from osxphotos.utils import (
download_url_to_temp_dir,
expand_and_validate_filepath,
is_http_url,
load_function,
download_url_to_dir,
)

__all__ = [
Expand Down Expand Up @@ -92,10 +93,7 @@ def convert(self, value, param, ctx):
# can't use TemporaryDirectory because it deletes the directory when it goes out of scope
# so use the system temp directory instead
try:
tmpdir = pathlib.Path(tempfile.gettempdir()) / "osxphotos"
tmpdir.mkdir(parents=True, exist_ok=True)
tmpdir = tempfile.mkdtemp(dir=tmpdir)
value = download_url_to_dir(value, tmpdir)
value = download_url_to_temp_dir(value)
except Exception as e:
self.fail(f"Could not download file from {value}: {e}")
else:
Expand Down Expand Up @@ -164,8 +162,7 @@ def convert(self, value, param, ctx):
if is_http_url(filename):
# need to retrieve file from URL and save it in a temp directory
try:
tmpdir = tempfile.TemporaryDirectory()
filename = download_url_to_dir(filename, tmpdir.name)
filename = download_url_to_temp_dir(filename)
except Exception as e:
self.fail(f"Could not download file from {filename}: {e}")

Expand Down
15 changes: 12 additions & 3 deletions osxphotos/phototemplate.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@
from .datetime_formatter import DateTimeFormatter
from .exiftool import ExifToolCaching
from .path_utils import sanitize_dirname, sanitize_filename, sanitize_pathpart
from .utils import expand_and_validate_filepath, load_function, uuid_to_shortuuid
from .utils import (
download_url_to_temp_dir,
expand_and_validate_filepath,
is_http_url,
load_function,
uuid_to_shortuuid,
)

__all__ = [
"RenderOptions",
Expand Down Expand Up @@ -254,7 +260,8 @@
+ "using Python string formatting codes specified by FORMAT; TYPE is one of: 'int', 'float', or 'str'. "
"For example, '{format:float:.1f,{exiftool:EXIF:FocalLength}}' will format focal length to 1 decimal place (e.g. '100.0'). ",
"{function}": "Execute a python function from an external file and use return value as template substitution. "
+ "Use in format: {function:file.py::function_name} where 'file.py' is the name of the python file and 'function_name' is the name of the function to call. "
+ "Use in format: {function:file.py::function_name} where 'file.py' is the path/name of the python file and 'function_name' is the name of the function to call. "
+ "The file name may also be url to a python file, e.g. '{function:https://raw.githubusercontent.com/RhetTbull/osxphotos/main/examples/template_function.py::example}'. "
+ "The function will be passed the PhotoInfo object for the photo. "
+ "See https://github.com/RhetTbull/osxphotos/blob/master/examples/template_function.py for an example of how to implement a template function.",
}
Expand Down Expand Up @@ -826,7 +833,7 @@ def get_field_values(
elif field == "function":
if subfield is None:
raise ValueError(
"SyntaxError: filename and function must not be null with {function::filename.py:function_name}"
"SyntaxError: filename and function must not be null with {function:filename.py::function_name}"
)
vals = self.get_template_value_function(
subfield, field_arg, self.options.caller
Expand Down Expand Up @@ -1393,6 +1400,8 @@ def get_template_value_function(

filename, funcname = subfield.split("::")

if is_http_url(filename):
filename = download_url_to_temp_dir(filename)
filename_validated = expand_and_validate_filepath(filename)
if not filename_validated:
raise ValueError(f"'{filename}' does not appear to be a file")
Expand Down
30 changes: 30 additions & 0 deletions osxphotos/tempdir.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Temporary directory for osxphotos session"""

from __future__ import annotations

import pathlib
import tempfile

_TEMPDIR = tempfile.TemporaryDirectory(prefix="osxphotos_")
TEMPDIR = pathlib.Path(_TEMPDIR.name)


def tempdir(subdir: str | None = None):
"""Return path to temporary directory that exists for the duration of the osxphotos session
Args:
subdir: optional subdirectory to create in temporary directory
Returns: pathlib.Path to temporary directory
"""
if subdir:
tmp = TEMPDIR / subdir
tmp.mkdir(parents=True, exist_ok=True)
return tmp
else:
return TEMPDIR


def cleanup():
"""Cleanup temporary directory"""
_TEMPDIR.cleanup()
31 changes: 28 additions & 3 deletions osxphotos/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@
import subprocess
import sys
import urllib.parse
from functools import cache
from plistlib import load as plistload
from typing import Callable, List, Optional, Tuple, TypeVar, Union
from uuid import UUID
import tempfile

import requests
import shortuuid

import osxphotos.tempdir as tempdir
from osxphotos.platform import get_macos_version, is_macos
from osxphotos.unicode import normalize_fs_path

Expand Down Expand Up @@ -593,11 +594,11 @@ def get_filename_from_url(url: str) -> str:

def download_url_to_dir(url: str, dir_path: str) -> str:
"""Download file from url to a directory path and return path to downloaded file
Args:
url: url to download
dir_path: path to directory where file should be downloaded (must exist)
Returns: path to downloaded file
Raises:
Expand All @@ -616,3 +617,27 @@ def download_url_to_dir(url: str, dir_path: str) -> str:
except Exception as e:
raise ValueError(f"Could not download {filename}: {e}") from e
return str(filename)


@cache
def download_url_to_temp_dir(url: str) -> str:
"""Download file from url to a temporary directory path and return path to downloaded file
Args:
url: url to download
Returns: path to downloaded file
Raises:
ValueError if download fails
Note: this function caches the result so that if called multiple times with the same URL,
the file will only be downloaded once.
"""

# need to retrieve file from URL and save it in a temp directory
# can't use TemporaryDirectory because it deletes the directory when it goes out of scope
# so use the system temp directory instead
# these files will be deleted when the system cleans the temp directory (usually on reboot)
tmpdir = tempdir.tempdir("downloads")
return download_url_to_dir(url, tmpdir)
31 changes: 31 additions & 0 deletions tests/test_tempdir.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Test tempdir module."""

from osxphotos.tempdir import cleanup, tempdir


def test_tempdir():
"""Test tempdir() and cleanup()"""
tmp = tempdir()
assert tmp.exists()

tmp2 = tempdir()
assert tmp2.exists()

assert tmp == tmp2

cleanup()
assert not tmp.exists()


def test_tempdir():
"""Test tempdir() and cleanup() with subdir"""
tmp = tempdir("foo")
assert tmp.exists()

tmp2 = tempdir("foo")
assert tmp2.exists()

assert tmp == tmp2

cleanup()
assert not tmp.exists()
10 changes: 10 additions & 0 deletions tests/test_template.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
""" Test template.py """
import os
import pathlib
import re

import pytest
Expand Down Expand Up @@ -1188,6 +1189,15 @@ def test_function(photosdb):
assert rendered == [f"{photo.original_filename}-FOO"]


def test_function_url(photosdb):
"""Test {function}"""
photo = photosdb.get_photo(UUID_DICT["favorite"])
rendered, _ = photo.render_template(
"{function:https://raw.githubusercontent.com/RhetTbull/osxphotos/main/examples/template_function.py::example}"
)
assert rendered == [f"{pathlib.Path(photo.original_filename).stem}#!"]


def test_function_bad(photosdb):
"""Test invalid {function}"""
photo = photosdb.get_photo(UUID_MULTI_KEYWORDS)
Expand Down

0 comments on commit 79806a8

Please sign in to comment.