Refactoring collection options (2)

This commit is contained in:
Cédric Leporcq 2021-11-13 18:20:08 +01:00
parent 0fdf09ea42
commit 27af9bb55e
5 changed files with 218 additions and 103 deletions

View File

@ -1,3 +1,11 @@
[Filters]
exclude= ["**/.directory", "**/.DS_Store"]
[Geolocation]
geocoder=Nominatim
prefer_english_names=hjkh
timeout=1
[Path] [Path]
# day_begins: what hour of the day you want the day to begin (only for # day_begins: what hour of the day you want the day to begin (only for
# classification purposes). Defaults at 0 as midnight. Can be # classification purposes). Defaults at 0 as midnight. Can be
@ -5,14 +13,7 @@
# be a number between 0-23') # be a number between 0-23')
day_begins=4 day_begins=4
# Path format
dirs_path={%Y}/{%m-%b}-{city}-{folder} dirs_path={%Y}/{%m-%b}-{city}-{folder}
name={%Y%m%d-%H%M%S}-%u{original_name}|%u{basename}.%l{ext} name={%Y%m%d-%H%M%S}-%u{original_name}|%u{basename}.%l{ext}
[Exclusions]
path1=**/.directory
path2=**/.DS_Store
[Geolocation]
geocoder=Nominatim
prefer_english_names=False
# timeout=1

View File

@ -8,7 +8,6 @@ import sys
import click import click
from ordigi import constants, log, LOG from ordigi import constants, log, LOG
from ordigi.config import Config
from ordigi.collection import Collection from ordigi.collection import Collection
from ordigi.geolocation import GeoLocation from ordigi.geolocation import GeoLocation
@ -116,13 +115,6 @@ def add_options(options):
return _add_options return _add_options
def _get_exclude(opt, exclude):
# if no exclude list was passed in we check if there's a config
if len(exclude) == 0:
exclude = opt['exclude']
return set(exclude)
def _get_paths(paths, root): def _get_paths(paths, root):
root = Path(root).expanduser().absolute() root = Path(root).expanduser().absolute()
if not paths: if not paths:

View File

@ -14,6 +14,7 @@ from pathlib import Path, PurePath
import inquirer import inquirer
from ordigi import LOG from ordigi import LOG
from ordigi.config import Config
from ordigi.database import Sqlite from ordigi.database import Sqlite
from ordigi.media import Medias from ordigi.media import Medias
from ordigi.images import Image, Images from ordigi.images import Image, Images
@ -220,6 +221,9 @@ class FPath:
Returns file path. Returns file path.
""" """
path_format = self.path_format path_format = self.path_format
# Each element in the list represents a folder.
# Fallback folders are supported and are nested lists.
path = [] path = []
path_parts = path_format.split('/') path_parts = path_format.split('/')
for path_part in path_parts: for path_part in path_parts:
@ -706,50 +710,58 @@ class Collection(SortMedias):
use_file_dates=False, use_file_dates=False,
): ):
day_begins=0 # TODO move to cli
max_deep=None cli_options = {
'album_from_folder': album_from_folder,
'cache': cache,
'dry_run': dry_run,
'exclude': exclude,
'extensions': extensions,
'glob': '**/*',
'interactive': interactive,
'ignore_tags': ignore_tags,
'use_date_filename': use_date_filename,
'use_file_dates': use_file_dates,
}
# Options
self.day_begins = day_begins
self.log = LOG.getChild(self.__class__.__name__) self.log = LOG.getChild(self.__class__.__name__)
self.glob = glob
# Check if collection path is valid # Check if collection path is valid
if not root.exists(): if not root.exists():
self.log.error(f'Collection path {root} does not exist') self.log.error(f'Collection path {root} does not exist')
sys.exit(1) sys.exit(1)
# def get_collection_config(root): self.root = root
# return Config(root.joinpath('.ordigi', 'ordigi.conf'))
# TODO Collection options # Get config options
# config = get_collection_config(root) config = self.get_config()
# opt = config.get_options() self.opt = config.get_options()
# exclude = _get_exclude(opt, kwargs['exclude'])
# path_format = opt['path_format'] # Set client options
# if kwargs['path_format']: for option, value in cli_options.items():
# path_format = kwargs['path_format'] self.opt[option] = value
self._set_cli_option('exclude', exclude)
self.db = CollectionDb(root) self.db = CollectionDb(root)
self.fileio = FileIO(dry_run) self.fileio = FileIO(self.opt['dry_run'])
self.paths = Paths( self.paths = Paths(
exclude, self.opt['exclude'],
extensions, self.opt['extensions'],
glob, self.opt['glob'],
interactive, self.opt['interactive'],
max_deep, self.opt['max_deep'],
) )
self.medias = Medias( self.medias = Medias(
self.paths, self.paths,
root, root,
album_from_folder, self.opt['album_from_folder'],
cache, self.opt['cache'],
self.db, self.db,
interactive, self.opt['interactive'],
ignore_tags, self.opt['ignore_tags'],
use_date_filename, self.opt['use_date_filename'],
use_file_dates, self.opt['use_file_dates'],
) )
# Features # Features
@ -758,21 +770,30 @@ class Collection(SortMedias):
self.medias, self.medias,
root, root,
self.db, self.db,
dry_run, self.opt['dry_run'],
interactive, self.opt['interactive'],
) )
# Attributes # Attributes
self.summary = Summary(self.root) self.summary = Summary(self.root)
self.theme = request.load_theme() self.theme = request.load_theme()
def get_config(self):
"""Get collection config"""
return Config(self.root.joinpath('.ordigi', 'ordigi.conf'))
def _set_cli_option(self, option, cli_option):
"""if client option is set overwrite collection option value"""
if cli_option:
self.opt['option'] = cli_option
def get_collection_files(self, exclude=True): def get_collection_files(self, exclude=True):
if exclude: if exclude:
exclude = self.paths.exclude exclude = self.paths.exclude
paths = Paths( paths = Paths(
exclude, exclude,
interactive=self.interactive, interactive=self.opt['interactive'],
) )
for file_path in paths.get_files(self.root): for file_path in paths.get_files(self.root):
yield file_path yield file_path
@ -932,7 +953,7 @@ class Collection(SortMedias):
files = os.listdir(directory) files = os.listdir(directory)
if len(files) == 0 and remove_root: if len(files) == 0 and remove_root:
self.log.info(f"Removing empty folder: {directory}") self.log.info(f"Removing empty folder: {directory}")
if not self.dry_run: if not self.opt['dry_run']:
os.rmdir(directory) os.rmdir(directory)
self.summary.append('remove', True, directory) self.summary.append('remove', True, directory)
@ -948,11 +969,14 @@ class Collection(SortMedias):
# Check db # Check db
self._init_check_db(loc) self._init_check_db(loc)
# if path format client option is set overwrite it
self._set_cli_option('path_format', path_format)
# Get medias data # Get medias data
subdirs = set() subdirs = set()
for src_path, metadata in self.medias.get_metadatas(src_dirs, imp=imp, loc=loc): for src_path, metadata in self.medias.get_metadatas(src_dirs, imp=imp, loc=loc):
# Get the destination path according to metadata # Get the destination path according to metadata
fpath = FPath(path_format, self.day_begins) fpath = FPath(path_format, self.opt['day_begins'])
metadata['file_path'] = fpath.get_path(metadata) metadata['file_path'] = fpath.get_path(metadata)
subdirs.add(src_path.parent) subdirs.add(src_path.parent)

View File

@ -1,15 +1,80 @@
import json
import re
from configparser import RawConfigParser from configparser import RawConfigParser
from os import path from os import path
from ordigi import constants from ordigi import constants
from geopy.geocoders import options as gopt from geopy.geocoders import options as gopt
# TODO make one method???
def check_option(getoption):
"""Check option type int or boolean"""
try:
getoption
except ValueError as e:
# TODO
return None
else:
return getoption
def check_json(getoption):
"""Check if json string is valid"""
try:
getoption
except json.JSONDecodeError as e:
# TODO
return None
else:
return getoption
def check_re(getoption):
"""Check if regex string is valid"""
try:
getoption
except re.error as e:
# TODO
return None
else:
return getoption
class Config: class Config:
"""Manage config file""" """Manage config file"""
def __init__(self, conf_path=constants.CONFIG_FILE, conf={}): # Initialize with default options
options: dict = {
'Console': {
'dry_run': False,
'interactive': False,
},
'Database': {
'cache': False,
'album_from_folder': False,
'ignore_tags': None,
'use_date_filename': False,
'use_file_dates': False,
},
'Filters': {
'exclude': None,
'extensions': None,
'glob': '**/*',
'max_deep': None,
},
'Geolocation': {
'geocoder': constants.DEFAULT_GEOCODER,
'prefer_english_names': False,
'timeout': gopt.default_timeout,
},
'Path': {
'day_begins': 0,
'path_format': constants.DEFAULT_PATH_FORMAT,
},
}
def __init__(self, conf_path=constants.CONFIG_FILE, conf=None):
self.conf_path = conf_path self.conf_path = conf_path
if conf == {}: if conf is None:
self.conf = self.load_config() self.conf = self.load_config()
if self.conf == {}: if self.conf == {}:
# Fallback to default config # Fallback to default config
@ -33,66 +98,94 @@ class Config:
conf.read(self.conf_path) conf.read(self.conf_path)
return conf return conf
def get_option(self, option, section): def is_option(self, section, option):
"""Get ConfigParser options"""
if section in self.conf and option in self.conf[section]: if section in self.conf and option in self.conf[section]:
return self.conf[section][option] return True
return False return False
def get_path_definition(self): @check_option
"""Returns a list of folder definitions. def _getboolean(self, section, option):
return self.conf.getboolean(section, option)
getboolean = check_option(_getboolean)
Each element in the list represents a folder. @check_option
Fallback folders are supported and are nested lists. def _getint(self, section, option):
return self.conf.getint(section, option)
getint = check_option(_getint)
:returns: string @check_json
""" def _getjson(self, section, option):
return json.loads(self.conf.get(section, option))
getjson = check_json(_getjson)
if 'Path' in self.conf: @check_re
if 'format' in self.conf['Path']: def _getre(self, section, option):
return self.conf['Path']['format'] return re.compile(self.conf.get(section, option))
elif 'dirs_path' and 'name' in self.conf['Path']: getre = check_re(_getre)
return self.conf['Path']['dirs_path'] + '/' + self.conf['Path']['name']
return constants.DEFAULT_PATH_FORMAT def get_option(self, section, option):
bool_options = {
'cache',
'dry_run',
'prefer_english_names',
'album_from_folder',
'interactive',
'use_date_filename',
'use_file_dates',
}
def get_options(self): int_options = {
"""Get config options 'day_begins',
:returns: dict 'max_deep',
""" 'timeout',
}
options = {} string_options = {
geocoder = self.get_option('geocoder', 'Geolocation') 'glob',
if geocoder and geocoder in ('Nominatim',): 'geocoder',
options['geocoder'] = geocoder }
else:
options['geocoder'] = constants.DEFAULT_GEOCODER
prefer_english_names = self.get_option('prefer_english_names', 'Geolocation') multi_options = {
if prefer_english_names: 'exclude',
options['prefer_english_names'] = bool(prefer_english_names) 'extensions',
else: 'ignore_tags',
options['prefer_english_names'] = False }
timeout = self.get_option('timeout', 'Geolocation') value = self.options[section][option]
if timeout: if self.is_option(section, option):
options['timeout'] = timeout if option in bool_options:
else: return self.getboolean(section, option)
options['timeout'] = gopt.default_timeout if option in int_options:
return self.getint(section, option)
if option == 'geocoder' and value in ('Nominatim',):
return self.conf[section][option]
if option == 'glob':
return self.conf[section][option]
if option == 'path_format':
return self.getre(section, option)
if option in multi_options:
return set(self.getjson(section, option))
options['path_format'] = self.get_path_definition() return value
options['day_begins'] = 0 if self.is_option('Path', 'name') and self.is_option('dirs_path', option):
options['max_deep'] = None # Path format is split in two parts
if 'Path' in self.conf: value = self.conf['Path']['dirs_path'] + '/' + self.conf['Path']['name']
if 'day_begins' in self.conf['Path']:
options['day_begins'] = int(self.conf['Path']['day_begins'])
if 'max_deep' in self.conf['Path']:
options['max_deep'] = int(self.conf['Path']['max_deep'])
options['exclude'] = [] return value
if 'Exclusions' in self.conf:
options['exclude'] = [value for key, value in self.conf.items('Exclusions')]
return options def get_options(self) -> dict:
"""Get config options"""
old_options = {}
for section in self.options:
for option in self.options[section]:
# Option is in section
# TODO make a function
value = self.get_option(section, option)
old_options[option] = value
self.options[section][option] = value
return old_options

View File

@ -50,11 +50,16 @@ class TestConfig:
config = Config(conf_path) config = Config(conf_path)
assert e.typename == 'MissingSectionHeaderError' assert e.typename == 'MissingSectionHeaderError'
def test_get_path_definition(self, conf): # def test_get_path_definition(self, conf):
""" # """
Get path definition from config # Get path definition from config
""" # """
config = Config(conf=conf) # config = Config(conf=conf)
path = config.get_path_definition() # path = config.get_path_definition()
assert path == '%u{%Y-%m}/{city}|{city}-{%Y}/{folders[:1]}/{folder}/{%Y-%m-%b-%H-%M-%S}-{basename}.%l{ext}' # assert path == '%u{%Y-%m}/{city}|{city}-{%Y}/{folders[:1]}/{folder}/{%Y-%m-%b-%H-%M-%S}-{basename}.%l{ext}'
def test_get_options(self, conf):
config = Config(conf=conf)
options = config.get_options()
assert isinstance(options, dict)
# assert isinstance(options['Path'], dict)