Skip to content

Commit 5a02974

Browse files
authored
Merge pull request #11 from deoren/deactivate-template-config-files
Template INI files, cmdline config path support
2 parents c58268a + 623a167 commit 5a02974

4 files changed

+156
-55
lines changed

mysql2sqlite.py

+83-23
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,23 @@
2323

2424
# parse command line arguments, 'sys.argv'
2525
import argparse
26+
import configparser
2627
import logging
2728
import logging.handlers
2829
import os
2930
import os.path
3031
import sqlite3
3132
import sys
3233

34+
from collections import OrderedDict
35+
3336

3437
app_name = 'mysql2sqlite'
3538

3639
# TODO: Configure formatter to log function/class info
37-
syslog_formatter = logging.Formatter('%(name)s - %(levelname)s - %(funcName)s - %(message)s')
38-
file_formatter = logging.Formatter('%(asctime)s - %(name)s - %(funcName)s - %(levelname)s - %(message)s')
39-
stdout_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(funcName)s - %(message)s')
40+
syslog_formatter = logging.Formatter('%(name)s - L%(lineno)d - %(levelname)s - %(funcName)s - %(message)s')
41+
file_formatter = logging.Formatter('%(asctime)s - %(name)s - L%(lineno)d - %(funcName)s - %(levelname)s - %(message)s')
42+
stdout_formatter = logging.Formatter('%(asctime)s - %(name)s - L%(lineno)d - %(levelname)s - %(funcName)s - %(message)s')
4043

4144
# Grab root logger and set initial logging level
4245
root_logger = logging.getLogger()
@@ -82,6 +85,34 @@
8285
log.debug("Logging initialized for %s", __name__)
8386

8487

88+
########################################################
89+
# Collect command-line arguments (e.g., passed by Cron)
90+
########################################################
91+
92+
parser = argparse.ArgumentParser(
93+
# Borrow docstring for this module
94+
description=__doc__.strip()
95+
)
96+
97+
parser.add_argument(
98+
'--config_file_dir',
99+
action='store',
100+
required=False,
101+
help='The directory path containing general and query config files.')
102+
103+
try:
104+
log.info('Parsing commandline options')
105+
args = parser.parse_args()
106+
except argparse.ArgumentError as error:
107+
log.exception("Unable to parse command-line arguments: %s", error)
108+
sys.exit(1)
109+
110+
if args.config_file_dir is not None:
111+
cmdline_config_file_dir = args.config_file_dir
112+
else:
113+
cmdline_config_file_dir = ""
114+
115+
85116
########################################
86117
# Modules - Third party
87118
########################################
@@ -115,7 +146,7 @@
115146
# CONSTANTS - Modify INI config files instead
116147
#######################################################
117148

118-
# Where this script being called from. We will try to load local copies of all
149+
# Where this script is called from. We will try to load local copies of all
119150
# dependencies from this location first before falling back to default
120151
# locations in order to support having all of the files bundled together for
121152
# testing and portable use.
@@ -124,27 +155,31 @@
124155
# The name of this script used (as needed) by error/debug messages
125156
script_name = os.path.basename(sys.argv[0])
126157

127-
# Read in configuration file. Attempt to read local copy first, then
128-
# fall back to using the copy provided by SVN+Symlinks
158+
general_config_file = 'mysql2sqlite_general.ini'
159+
query_config_file = 'mysql2sqlite_queries.ini'
129160

130-
# TODO: Replace with command-line options
131-
default_config_file_dir = '/etc/whyaskwhy.org/mysql2sqlite/config'
161+
# Listed in in order of precedence: first match in list wins
162+
# https://stackoverflow.com/a/28231217
163+
# https://www.blog.pythonlibrary.org/2016/03/24/python-201-ordereddict/
164+
config_file_paths = OrderedDict({
165+
'cmdline_config_file_dir': cmdline_config_file_dir,
166+
'local_config_file_dir': script_path,
167+
'user_config_file_dir': os.path.expanduser('~/.config/mysql2sqlite'),
168+
'default_config_file_dir': '/etc/mysql2sqlite',
169+
})
132170

133-
general_config_file = {}
134-
general_config_file['name'] = 'mysql2sqlite_general.ini'
135-
general_config_file['local'] = os.path.join(script_path, general_config_file['name'])
136-
general_config_file['global'] = os.path.join(default_config_file_dir, general_config_file['name'])
171+
general_config_file_candidates = []
172+
query_config_file_candidates = []
173+
for key in reversed(config_file_paths):
137174

138-
queries_config_file = {}
139-
queries_config_file['name'] = 'mysql2sqlite_queries.ini'
140-
queries_config_file['local'] = os.path.join(script_path, queries_config_file['name'])
141-
queries_config_file['global'] = os.path.join(default_config_file_dir, queries_config_file['name'])
175+
general_config_file_candidates.append(
176+
os.path.join(config_file_paths[key], general_config_file))
177+
178+
query_config_file_candidates.append(
179+
os.path.join(config_file_paths[key], query_config_file))
142180

143181
# Prefer the local copy over the "global" one by loading it last (where the
144182
# second config file overrides or "shadows" settings from the first)
145-
general_config_file_candidates = [general_config_file['global'], general_config_file['local']]
146-
147-
queries_config_file_candidates = [queries_config_file['global'], queries_config_file['local']]
148183

149184
# Generate configuration setting options
150185
log.debug(
@@ -153,17 +188,42 @@
153188

154189
log.debug(
155190
"Passing in these query config file locations for evalution: %s",
156-
queries_config_file_candidates)
191+
query_config_file_candidates)
157192

158193
# Generate configuration setting options
159194
log.info('Parsing config files')
160-
general_settings = m2slib.GeneralSettings(general_config_file_candidates)
161-
query_settings = m2slib.QuerySettings(queries_config_file_candidates)
195+
196+
# Apply handler early so that console logging is enabled prior to parsing
197+
# configuration files. The provided filter configuration allows logging
198+
# warning and error messages only while the settings object has yet to be
199+
# defined.
200+
app_logger.addHandler(console_handler)
201+
console_handler.addFilter(m2slib.ConsoleFilterFunc(settings=None))
202+
203+
try:
204+
general_settings = m2slib.GeneralSettings(general_config_file_candidates)
205+
except configparser.NoSectionError as error:
206+
log.exception("Error parsing configuration file: %s", error)
207+
sys.exit(1)
208+
except IOError as error:
209+
log.exception("Error reading configuration file: %s", error)
210+
sys.exit(1)
211+
212+
try:
213+
query_settings = m2slib.QuerySettings(query_config_file_candidates)
214+
except configparser.NoSectionError as error:
215+
log.exception("Error parsing configuration file: %s", error)
216+
sys.exit(1)
217+
except IOError as error:
218+
log.exception("Error reading configuration file: %s", error)
219+
sys.exit(1)
220+
162221

163222
# Now that the settings object has been properly created, lets use it to
164223
# finish configuring console logging for the main application logger.
224+
console_handler.removeFilter(m2slib.ConsoleFilterFunc)
165225
console_handler.addFilter(m2slib.ConsoleFilterFunc(settings=general_settings))
166-
app_logger.addHandler(console_handler)
226+
167227

168228
####################################################################
169229
# Troubleshooting config file flag boolean conversion
File renamed without changes.

mysql2sqlite_lib.py

+73-32
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,19 @@
6868
# Classes
6969
#######################################################
7070

71+
# TODO: Consider replacing class/functionality here with Python 3.2+ support for
72+
# mapping API
73+
#
74+
# This support allows accessing a ConfigParser instance as a single
75+
# dictionary with separate nested dictionaries for each section. In short, the
76+
# two classes listed here do not appear to be needed any longer?
77+
#
78+
# Well, except for perhaps string to boolean conversion?
79+
#
80+
# https://pymotw.com/3/configparser/
81+
# https://docs.python.org/3.5/library/configparser.html#mapping-protocol-access
82+
83+
7184
class GeneralSettings(object):
7285

7386
"""
@@ -79,18 +92,22 @@ def __init__(self, config_file_list):
7992

8093
self.log = log.getChild(self.__class__.__name__)
8194

82-
try:
83-
parser = configparser.SafeConfigParser()
84-
processed_files = parser.read(config_file_list)
95+
parser = configparser.ConfigParser()
96+
processed_files = parser.read(config_file_list)
8597

86-
except configparser.ParsingError as error:
87-
self.log.exception("Unable to parse config file: %s", error)
88-
sys.exit(1)
89-
90-
else:
98+
if processed_files:
9199
self.log.debug("CONFIG: Config files processed: %s",
92100
processed_files)
101+
else:
102+
self.log.error("Failure to read config files; "
103+
"See provided templates, modify and place in one of the "
104+
"supported locations: %s",
105+
", ".join(config_file_list))
93106

107+
raise IOError("Failure to read config files; "
108+
"See provided templates, modify and place in one of the "
109+
"supported locations: ",
110+
", ".join(config_file_list))
94111

95112
# Begin building object by creating dictionary member attributes
96113
# from config file sections/values.
@@ -123,7 +140,7 @@ def __init__(self, config_file_list):
123140
except configparser.NoSectionError as error:
124141

125142
self.log.exception("Unable to parse config file: %s", error)
126-
sys.exit(1)
143+
raise
127144

128145
class QuerySettings(object):
129146

@@ -136,17 +153,26 @@ def __init__(self, config_file_list):
136153

137154
self.log = log.getChild(self.__class__.__name__)
138155

139-
try:
140-
parser = configparser.SafeConfigParser()
141-
processed_files = parser.read(config_file_list)
156+
parser = configparser.SafeConfigParser()
157+
processed_files = parser.read(config_file_list)
142158

143-
except configparser.ParsingError as error:
144-
self.log.exception("Unable to parse config file: %s", error)
145-
sys.exit(1)
159+
# We've reached this point if no errors were thrown attempting
160+
# to read the list of config files. We now need to count the
161+
# number of parsed files and if zero, attempt to resolve why.
146162

147-
else:
163+
if processed_files:
148164
self.log.debug("CONFIG: Config files processed: %s",
149165
processed_files)
166+
else:
167+
self.log.error("Failure to read config files; "
168+
"See provided templates, modify and place in one of the "
169+
"supported locations: %s",
170+
", ".join(config_file_list))
171+
172+
raise IOError("Failure to read config files; "
173+
"See provided templates, modify and place in one of the "
174+
"supported locations: ",
175+
", ".join(config_file_list))
150176

151177
# Setup an empty dictionary that we'll then populate with nested
152178
# dictionaries
@@ -164,8 +190,10 @@ def __init__(self, config_file_list):
164190

165191
except configparser.NoSectionError as error:
166192

167-
self.log.exception("Unable to parse config file: %s", error)
168-
sys.exit(1)
193+
self.log.exception(
194+
"Unable to parse '%s' section of config file: %s",
195+
section, error)
196+
raise
169197

170198
class ConsoleFilterFunc(logging.Filter):
171199

@@ -179,21 +207,34 @@ def __init__(self, settings):
179207
#print("Just proving that this function is being called")
180208

181209
def filter(self, record):
182-
if self.settings.flags['display_console_error_messages'] and record.levelname == 'ERROR':
183-
#print("Error messages enabled")
184-
return True
185-
if self.settings.flags['display_console_warning_messages'] and record.levelname == 'WARNING':
186-
#print("Warning messages enabled")
187-
return True
188-
if self.settings.flags['display_console_info_messages'] and record.levelname == 'INFO':
189-
#print("Info messages enabled")
190-
return True
191-
if self.settings.flags['display_console_debug_messages'] and record.levelname == 'DEBUG':
192-
#print("Debug messages enabled")
193-
return True
210+
211+
# If filter is not passed a settings object then fall back
212+
# to default values. This may occur if the configuration files are
213+
# not able to be read for one reason or another. In that situation
214+
# we want the error output to be as verbose as possible.
215+
if self.settings:
216+
if self.settings.flags['display_console_error_messages'] and record.levelname == 'ERROR':
217+
#print("Error messages enabled")
218+
return True
219+
if self.settings.flags['display_console_warning_messages'] and record.levelname == 'WARNING':
220+
#print("Warning messages enabled")
221+
return True
222+
if self.settings.flags['display_console_info_messages'] and record.levelname == 'INFO':
223+
#print("Info messages enabled")
224+
return True
225+
if self.settings.flags['display_console_debug_messages'] and record.levelname == 'DEBUG':
226+
#print("Debug messages enabled")
227+
return True
228+
else:
229+
#print("No matches")
230+
return False
194231
else:
195-
#print("No matches")
196-
return False
232+
# Go with hard-coded default of displaying warning and error
233+
# messages until the settings object has been defined.
234+
if record.levelname == 'ERROR':
235+
return True
236+
if record.levelname == 'WARNING':
237+
return True
197238

198239
#######################################################
199240
# Functions
File renamed without changes.

0 commit comments

Comments
 (0)