From 6bec182703885761699f6d53bc9034933b03197e Mon Sep 17 00:00:00 2001
From: David Luevano Alvarado <david@luevano.xyz>
Date: Sun, 17 Apr 2022 19:39:38 -0600
Subject: add initial logging capabilities

still need to add logging to rest of the program: builder, database, discovery, page, parser and rest of the pyssg (build part)
---
 README.md                        |  1 +
 src/pyssg/__init__.py            | 17 ++++++++++++++++-
 src/pyssg/__main__.py            |  2 ++
 src/pyssg/configuration.py       | 14 ++++++++++++--
 src/pyssg/per_level_formatter.py | 28 ++++++++++++++++++++++++++++
 src/pyssg/pyssg.py               | 26 ++++++++++++++++++++------
 src/pyssg/utils.py               | 15 ++++++++++-----
 7 files changed, 89 insertions(+), 14 deletions(-)
 create mode 100644 src/pyssg/per_level_formatter.py

diff --git a/README.md b/README.md
index 7845805..2163a95 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,7 @@ Inspired (initially) by Roman Zolotarev's [`ssg5`](https://rgz.ee/bin/ssg5) and
 - [x] Avoid the program to freak out when there are directories created in advance.
 - [ ] Provide more meaningful error messages when you are missing mandatory tags in your `.md` files.
 - [ ] More complex directory structure to support multiple subdomains and different types of pages.
+- [ ] Add option/change to using an SQL database instead of the custom solution.
 
 ### Markdown features
 
diff --git a/src/pyssg/__init__.py b/src/pyssg/__init__.py
index 074ae72..cdd4cd1 100644
--- a/src/pyssg/__init__.py
+++ b/src/pyssg/__init__.py
@@ -1,4 +1,19 @@
 from .pyssg import main
+import logging
+from logging import Logger, StreamHandler
+from .per_level_formatter import PerLevelFormatter
 
 
-__all__ = ['main']
+# since this is the root package, setup the logger here,
+#   set DEBUG here for testing purposes, can't make it 
+#   dynamic yet (with a flag, for example)
+__LOG_LEVEL: int = logging.INFO
+log: Logger = logging.getLogger(__name__)
+log.setLevel(__LOG_LEVEL)
+ch: StreamHandler = StreamHandler()
+ch.setLevel(__LOG_LEVEL)
+ch.setFormatter(PerLevelFormatter())
+log.addHandler(ch)
+
+# not meant to be used as a package, so just give main
+__all__ = ['main']
\ No newline at end of file
diff --git a/src/pyssg/__main__.py b/src/pyssg/__main__.py
index 9ed4d74..5213889 100644
--- a/src/pyssg/__main__.py
+++ b/src/pyssg/__main__.py
@@ -1,5 +1,7 @@
 from .pyssg import main
 
 
+# since this is not used as a package, rather it's used as a command line tool,
+#   this is never called because pyssg:main is called directly when running pyssg
 if __name__ == '__main__':
     main()
diff --git a/src/pyssg/configuration.py b/src/pyssg/configuration.py
index dd6bfaa..a721dba 100644
--- a/src/pyssg/configuration.py
+++ b/src/pyssg/configuration.py
@@ -3,6 +3,10 @@ from importlib.metadata import version
 from importlib.resources import path as rpath
 from datetime import datetime, timezone
 from configparser import ConfigParser
+import logging
+from logging import Logger
+
+log: Logger = logging.getLogger(__name__)
 
 
 DEFAULT_CONFIG_PATH = '$XDG_CONFIG_HOME/pyssg/config.ini'
@@ -12,25 +16,31 @@ VERSION = version('pyssg')
 def __check_well_formed_config(config: ConfigParser) -> None:
     default_config: ConfigParser = ConfigParser()
     with rpath('pyssg.plt', 'default.ini') as p:
+        log.debug('reading config file "%s"', p)
         default_config.read(p)
 
     for section in default_config.sections():
+        log.debug('checking section "%s"', section)
         if not config.has_section(section):
-            print(f'config does not have section "{section}"')
+            log.error('config does not have section "%s"', section)
             sys.exit(1)
         for option in default_config.options(section):
+            log.debug('checking option "%s"', option)
             if not config.has_option(section, option):
-                print(f'config does not have option "{option}" in section "{section}"')
+                log.error('config does not have option "%s" in section "%s"', option, section)
                 sys.exit(1)
 
 
 def get_parsed_config(path: str) -> ConfigParser:
     config: ConfigParser = ConfigParser()
+    log.debug('reading config file "%s"', path)
     config.read(path)
 
+    log.debug('checking that config file is well formed')
     __check_well_formed_config(config)
 
     # set other required options
+    log.debug('setting extra config options')
     config.set('fmt', 'rss_date', '%%a, %%d %%b %%Y %%H:%%M:%%S GMT')
     config.set('fmt', 'sitemap_date', '%%Y-%%m-%%d')
     config.set('info', 'version', VERSION)
diff --git a/src/pyssg/per_level_formatter.py b/src/pyssg/per_level_formatter.py
new file mode 100644
index 0000000..2010483
--- /dev/null
+++ b/src/pyssg/per_level_formatter.py
@@ -0,0 +1,28 @@
+import logging
+from logging import Formatter
+
+
+class PerLevelFormatter(logging.Formatter):
+    # colors for the terminal in ansi
+    yellow = "\x1b[33m"
+    red = "\x1b[31m"
+    bold_red = "\x1b[31;1m"
+    reset = "\x1b[0m"
+
+    DATE_FMT = '%Y-%m-%d %H:%M:%S'
+    COMMON_FMT = '[%(levelname)s] [%(module)s:%(funcName)s:%(lineno)d]: %(message)s'
+    FORMATS = {
+        logging.DEBUG: COMMON_FMT,
+        logging.INFO: '%(message)s',
+        logging.WARNING: f'{yellow}{COMMON_FMT}{reset}',
+        logging.ERROR: f'{red}{COMMON_FMT}{reset}',
+        logging.CRITICAL: f'{bold_red}{COMMON_FMT}{reset}'
+    }
+
+
+    def format(self, record: str) -> str:
+        fmt: str = self.FORMATS.get(record.levelno)
+        formatter: Formatter = logging.Formatter(
+            fmt=fmt, datefmt=self.DATE_FMT, style='%')
+
+        return formatter.format(record)
diff --git a/src/pyssg/pyssg.py b/src/pyssg/pyssg.py
index 9b82231..958b397 100644
--- a/src/pyssg/pyssg.py
+++ b/src/pyssg/pyssg.py
@@ -1,5 +1,7 @@
 import os
 import sys
+import logging
+from logging import Logger, StreamHandler
 from importlib.resources import path as rpath
 from typing import Union
 from configparser import ConfigParser
@@ -10,43 +12,53 @@ from yafg import YafgExtension
 from MarkdownHighlight.highlight import HighlightExtension
 from markdown_checklist.extension import ChecklistExtension
 
+from .per_level_formatter import PerLevelFormatter
 from .utils import create_dir, copy_file, sanity_check_path
 from .arg_parser import get_parsed_arguments
 from .configuration import get_parsed_config, DEFAULT_CONFIG_PATH, VERSION
 from .database import Database
 from .builder import Builder
 
+log: Logger = logging.getLogger(__name__)
+
 
 def main() -> None:
     args: dict[str, Union[str, bool]] = vars(get_parsed_arguments())
+
     if not len(sys.argv) > 1:
-        print(f'pyssg v{VERSION} - no arguments passed, --help for more')
+        log.info('pyssg v%s - no arguments passed, --help for more', VERSION)
         sys.exit(0)
 
     if args['version']:
-        print(f'pyssg v{VERSION}')
+        log.info('pyssg v%s', VERSION)
         sys.exit(0)
 
+    log.debug('checking config file path')
     config_path: str = args['config'] if args['config'] else DEFAULT_CONFIG_PATH
     config_path = os.path.normpath(os.path.expandvars(config_path))
     sanity_check_path(config_path)
     config_dir, _ = os.path.split(config_path)
+    log.debug('checked config file path, final config path "%s"', config_path)
 
     if args['copy_default_config']:
+        log.info('copying default config file')
         create_dir(config_dir)
         with rpath('pyssg.plt', 'default.ini') as p:
             copy_file(p, config_path)
         sys.exit(0)
 
     if not os.path.exists(config_path):
-        print(f'''config file does't exist in path "{config_path}"; make sure
-                  the path is correct; use --copy-default-config to if you
-                  haven't already''')
+        log.error('config file does\'t exist in path "%s"; make sure'
+                  ' the path is correct; use --copy-default-config if it\'s the'
+                  ' first time if you haven\'t already', config_path)
         sys.exit(1)
 
+    log.debug('parsing config file')
     config: ConfigParser = get_parsed_config(config_path)
+    log.debug('parsed config file')
 
     if args['init']:
+        log.info('initializing the directory structure and copying over templates')
         create_dir(config.get('path', 'src'))
         create_dir(os.path.join(config.get('path', 'dst'), 'tag'), True)
         create_dir(config.get('path', 'plt'))
@@ -55,12 +67,15 @@ def main() -> None:
                             'tag.html',
                             'rss.xml',
                             'sitemap.xml')
+        log.debug('list of files to copy over: (%s)', ', '.join(files))
         for f in files:
             plt_file: str = os.path.join(config.get('path', 'plt'), f)
             with rpath('pyssg.plt', f) as p:
                 copy_file(p, plt_file)
         sys.exit(0)
 
+    # TODO: add logging to all of the build part, that includes the builder,
+    #   database, discovery, page and parser
     if args['build']:
         # start the db
         db: Database = Database(os.path.join(config.get('path', 'src'), '.files'))
@@ -73,7 +88,6 @@ def main() -> None:
                                        trim_blocks=True,
                                        lstrip_blocks=True)
 
-
         # md extensions
         exts: list = ['extra',
                       'meta',
diff --git a/src/pyssg/utils.py b/src/pyssg/utils.py
index 8e5d90e..2194fe1 100644
--- a/src/pyssg/utils.py
+++ b/src/pyssg/utils.py
@@ -1,6 +1,10 @@
 import os
 import sys
 import shutil
+import logging
+from logging import Logger
+
+log: Logger = logging.getLogger(__name__)
 
 
 def create_dir(path: str, p: bool=False) -> None:
@@ -9,20 +13,21 @@ def create_dir(path: str, p: bool=False) -> None:
             os.makedirs(path)
         else:
             os.mkdir(path)
-        print(f'created directory "{path}"')
+        log.info('created directory "%s"', path)
     except FileExistsError:
-        print(f'directory "{path}" already exists')
+        log.info('directory "%s" already exists, ignoring', path)
 
 
 def copy_file(src: str, dst: str) -> None:
     if not os.path.exists(dst):
         shutil.copy(src, dst)
-        print(f'copied file "{src}" to "{dst}"')
+        log.info('copied file "%s" to "%s"', src, dst)
     else:
-        print(f'"{dst}" already exists')
+        log.info('file "%s" already exists, ignoring', dst)
 
 
 def sanity_check_path(path: str) -> None:
     if '$' in  path:
-        print(f'"$" character found in path: "{path}"; could be due to non-existant env var.')
+        log.error('"$" character found in path "%s";'
+                  ' could be due to non-existant env var.', path)
         sys.exit(1)
-- 
cgit v1.2.3-70-g09d2