Repository: miraisolutions/secretsanta

Issue:
Add basic logging example

Discussion:

Patch:
diff --git a/README.md b/README.md
index 7a3a0d7..1b27abe 100644
--- a/README.md
+++ b/README.md
@@ -49,12 +49,12 @@ A couple of useful references about virtual environments if you've never used th
 * [Virtual environments](https://docs.python-guide.org/dev/virtualenvs/)
 * [Creating virtual environments](https://packaging.python.org/tutorials/installing-packages/#creating-virtual-environments)
 
-Install support for virtual environments with Python 3.x if you don't have it yet:
+Install support for virtual environments with Python 3.x if you don't have it yet. Below we assume Python 3.8:
 ```bash
-sudo apt-get install python3-venv
+sudo apt-get install python3.8-venv
 ```
 
-Configure the PyCharm project with a Python 3 virtual environment under
+Configure the PyCharm project with a Python 3.8 virtual environment under
 ```
 File > Settings > Project > Project interpreter
 ```
@@ -98,7 +98,7 @@ pip install --upgrade pip
 #> ...
 
 pip --version
-#> pip 20.0.2 from /home/localuser/PyCharm/secretsanta/venv/lib/python3.6/site-packages/pip (python 3.6)
+#> pip 20.3.3 from /home/mirai/PycharmProjects/secretsanta/venv/lib/python3.8/site-packages/pip (python 3.8)
 ```
 
 Install [pip-tools](<https://github.com/jazzband/pip-tools>):
@@ -112,10 +112,11 @@ pip list
 #> Package       Version
 #> ------------- -------
 #> Click         7.0    
-#> pip           20.0.2 
-#> pip-tools     5.0.0  
-#> setuptools    46.1.3 
-#> six           1.11.0 
+#> pip           21.0.1 
+#> pip-tools     5.5-0
+#> pkg-resources 0.0.0  
+#> setuptools    51.0.0 
+#> six           1.15.0 
 ```
 
 Re-generate `requirements.txt` from `requirements.in`:
@@ -341,6 +342,31 @@ a public repository, we do not use this functionality here._
 
 ### Miscellaneous
 
+##### Logging
+
+The `logging` package is used to track events after running the project. The main logged events (levels) in Secret Santa are: errors, warnings, and participants info. A log level is set as an environment variable, e.g.:
+```bash
+os.environ["level"] = "ERROR"
+```
+
+
+All logs activities are collected into a log file that is initiated at the beginning of the code:
+```bash
+logging.basicConfig(filename = path_to_file, level = level, format = '%(asctime)s %(levelname)s %(message)s',
+                               datefmt = '%Y/%m/%d %I:%M:%S %p')
+```
+A logger is then set:
+```bash
+logger = logging.getLogger(__name__)
+```
+All functions used afterwards refer to this logger:
+```bash
+logger.error("Error message")
+logger.warning("Warning message")
+logger.info("Info")
+```
+The log file is automatically created in the `log_files` directory and can be inspected after the project run is complete. 
+
 * `MANIFEST.in` specifies extra files that shall be included in a source distribution.
 * Badges: This README features various badges (at the beginning), including a build status badge and a code coverage
 status badge.
diff --git a/run/run.py b/run/run.py
new file mode 100644
index 0000000..6b6eae6
--- /dev/null
+++ b/run/run.py
@@ -0,0 +1,31 @@
+import os
+from secretsanta.main.funs import make_santa_dict
+
+participants = {
+    'Stephanie': 'stephanie',
+    'Simon': 'simon',
+    'Gustavo': 'gustavo',
+    'Nicola': 'nicola',
+    'Guido': 'guido',
+    'Francesca': 'francesca',
+    'Gabriel': 'gabriel',
+    'Roland': 'roland',
+    'Nikki': 'nicoletta',
+    'Peter': 'peter',
+    'Martin': 'martin',
+    'Riccardo': 'riccardo'
+}
+
+for name in participants.keys() :
+    participants[name] += '@acme-example.com'
+
+# remove previously created file
+file_to_rm = os.listdir("./log_files")
+file_rm = "./log_files/" + str(file_to_rm[0])
+os.remove(file_rm)
+
+# set logging level as environment variable
+os.environ["level"] = "DEBUG"
+log_level = os.environ.get('log_level', os.getenv("level")).upper()
+print(make_santa_dict(participants, seed = None, verbose = True, level = log_level))
+
diff --git a/secretsanta/main/funs.py b/secretsanta/main/funs.py
index ee5c011..18e6f9f 100644
--- a/secretsanta/main/funs.py
+++ b/secretsanta/main/funs.py
@@ -1,19 +1,20 @@
 import datetime
+import logging
 import smtplib
 from contextlib import suppress
+from secretsanta.main.core import SecretSanta
+from secretsanta.main.utils import setup_logging
 from typing import Optional, Dict, Tuple, List, Union, Any
 
 import numpy as np
 
-from secretsanta.main.core import SecretSanta
-
-
 # PyCharm: ctrl-p inside parentheses shows function args!
 
 
 # mypy is missing a library stub file for module 'numpy' and will complain about it
 # workaround: append --ignore-missing-imports to the mypy call (see https://github.com/python/mypy/issues/3905)
-def make_santa_dict(dictionary: Dict[str, str], seed: Optional[int] = None, verbose: bool = False) -> Dict[str, str]:
+def make_santa_dict(dictionary: Dict[str, str], seed: Optional[int] = None,
+                    verbose: bool = False, level: str = "ERROR") -> Dict[str, str]:
     # type triple-quotes and press enter to generate empty docstring stub
     """
     creates a random secret santa assignment as a dictionary from an initial dictionary of the participants' names and\
@@ -38,6 +39,10 @@ def make_santa_dict(dictionary: Dict[str, str], seed: Optional[int] = None, verb
     # a@acme.com and b's to c@acme.com.
     ######
 
+    setup_logging(level)
+    logger = logging.getLogger(__name__)
+    #print(__name__): secretsanta.main.funs
+
     # We need to map each person to some e-mail address, so we start by getting a list of all names
     # unpack dict_keys object into list literal (no control over order!)
     # https://stackoverflow.com/questions/16819222/how-to-return-dictionary-keys-as-a-list-in-python
@@ -52,14 +57,17 @@ def make_santa_dict(dictionary: Dict[str, str], seed: Optional[int] = None, verb
 
     np.random.seed(seed)
 
+    if len(dictionary) == 1:
+        logger.error("Only one person listed")
+        raise ValueError("Only one person listed")
+    if len(dictionary) <= 3:
+        logger.warning("Too few people, assignment will be deterministic")
+
     # "dict"s are always unordered, therefore iterating through them has unpredictable order
     for name in dictionary:
         # print(dictionary.get(name))
         if verbose:
-            print(name)
-        # PyCharm: Alt+Enter (on variable definition) -> Add type hint - automatically generates type given variable
-        # assignment
-        # `name` is the person getting a present, we want to pick their secret santa from the available participants
+            logger.info(str(name))
         pick = names.copy()
         if len(pick) == 1:
             # if this is the last person in the list, we only have one choice left
@@ -88,7 +96,7 @@ def make_santa_dict(dictionary: Dict[str, str], seed: Optional[int] = None, verb
             # randomly pick a participant
             picked = np.random.choice(pick, 1)[0]
             if verbose:
-                print(picked)
+                logger.info(str(picked))
             # set `name`'s value in the result to the picked participant's e-mail.
             senddict[name] = dictionary[picked]
             names.remove(picked)
diff --git a/secretsanta/main/utils.py b/secretsanta/main/utils.py
new file mode 100644
index 0000000..5f3780b
--- /dev/null
+++ b/secretsanta/main/utils.py
@@ -0,0 +1,16 @@
+import logging
+import time
+import os
+
+
+def setup_logging(level: str = "ERROR"):
+    """
+sets up logging configuration.
+
+:param level: logging level
+    """
+
+    name_file = 'secretsanta_' + time.strftime("%Y%m%d-%H%M%S") +'.log'
+    path_file = os.path.join('./log_files/', name_file)
+    logging.basicConfig(filename =path_file, level = level, format = '%(asctime)s %(levelname)s %(message)s',
+                               datefmt = '%Y/%m/%d %I:%M:%S %p')


Test patch:
diff --git a/tests/main/test_funs.py b/tests/main/test_funs.py
index 17896da..2467621 100644
--- a/tests/main/test_funs.py
+++ b/tests/main/test_funs.py
@@ -9,6 +9,7 @@ from hypothesis.strategies import text, lists, integers, characters
 class MakeSantaDict(unittest.TestCase):
     # we specify some character classes to avoid unprintable characters that cause issues when used as dictionary keys.
     # the min / max parameters passed to integers match the accepted range for seeds.
+
     @given(lists(text(alphabet=characters(whitelist_categories=["Lu", "Ll", "Nd", "Pc", "Pd"],
                                           whitelist_characters=["@"]), min_size=2, max_size=20),  min_size=2, max_size=10, unique=True),
            integers(min_value=0, max_value=2**32 - 1))
diff --git a/tests/main/test_logging.py b/tests/main/test_logging.py
new file mode 100644
index 0000000..743985c
--- /dev/null
+++ b/tests/main/test_logging.py
@@ -0,0 +1,54 @@
+import unittest
+import os
+from hypothesis import given
+from secretsanta.main.funs import make_santa_dict
+from hypothesis.strategies import text, lists, integers, characters, sampled_from
+
+class TestLogging(unittest.TestCase):
+    # test error
+    def test_error_single_participant(self):
+        participants_dict = {'Stephanie': 'Stephanie'}
+        os.environ["level"] = "ERROR"
+        log_level = os.environ.get('log_level', os.getenv("level")).upper()
+        with self.assertRaises(ValueError):
+            make_santa_dict(participants_dict, seed=None, verbose=True, level=log_level)
+
+    # test warning
+    ls = ['Stephanie', 'Simon', 'Gustavo']
+    @given(lists(sampled_from(ls), min_size=2, max_size=2, unique=True))
+
+    def test_warning(self, participants):
+        # convert list to dict
+        participants_dict = {i: i for i in participants}
+        # check if directory exists, if not then create it
+        check_dir = os.path.exists('log_files')
+        if not check_dir:
+            directory = "log_files"
+            path = os.path.join('.', directory)
+            os.makedirs(path)
+        os.environ["level"] = "DEBUG"
+        log_level = os.environ.get('log_level', os.getenv("level")).upper()
+        with self.assertLogs('secretsanta.main.funs', level='WARNING') as cm:
+            make_santa_dict(participants_dict, seed=1, verbose=True, level=log_level)
+        self.assertEqual(cm.output[0], 'WARNING:secretsanta.main.funs:Too few people, assignment will be deterministic')
+
+    # test info
+    @given(lists(text(alphabet=characters(whitelist_categories=["Lu", "Ll", "Nd", "Pc", "Pd"],
+                                              whitelist_characters=["@"]), min_size=2, max_size=20),
+                     min_size=2, max_size=10, unique=True))
+
+    def test_info(self, test_list):
+        #convert list to dict
+        participants_dict = {i: i for i in test_list}
+        # check if directory exists, if not then create it
+        check_dir = os.path.exists('log_files')
+        if not check_dir:
+            directory = "log_files"
+            path = os.path.join('.', directory)
+            os.makedirs(path)
+        os.environ["level"] = "DEBUG"
+        log_level = os.environ.get('log_level', os.getenv("level")).upper()
+        if log_level == "INFO":
+            with self.assertLogs('secretsanta.main.funs', level='INFO') as cm:
+                make_santa_dict(participants_dict, seed=1, verbose=True, level=log_level)
+            self.assertEqual(cm.output[0], log_level + ':secretsanta.main.funs:' + test_list[0])
\ No newline at end of file