diff --git a/Student/kristianjf/lesson03/Class 3 - Decorators.ipynb b/Student/kristianjf/lesson03/Class 3 - Decorators.ipynb new file mode 100644 index 0000000..b0fbffb --- /dev/null +++ b/Student/kristianjf/lesson03/Class 3 - Decorators.ipynb @@ -0,0 +1,655 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "def add(a,b):\n", + " return a+b\n", + " \n", + "def sub(a,b):\n", + " return a-b\n", + "\n", + "def mul(a,b):\n", + " return a*b" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### Notes\n", + "\n", + "Using a decorator around the function below allowed us to wrap additional functionality around the add function. Remember, positional arguments and keyword arguments can be passed between methods/functions." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Investigate this further" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "def myprint(func):\n", + " def logged(*args, **kwargs):\n", + " print(\"Function {} called\".format(func.__name__))\n", + " if args:\n", + " print(\"\\twith args: {}\".format(args))\n", + " if kwargs:\n", + " print(\"\\twith kwargs: {}\".format(kwargs))\n", + " result = func(*args, **kwargs)\n", + " print(\"\\t Result --> {}\".format(result))\n", + " return result\n", + " return logged" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'add' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mlogging_add\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmyprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0madd\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;31mNameError\u001b[0m: name 'add' is not defined" + ] + } + ], + "source": [ + "logging_add = myprint(add)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'logging_add' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mlogging_add\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m3\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;36m4\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;31mNameError\u001b[0m: name 'logging_add' is not defined" + ] + } + ], + "source": [ + "logging_add(3,4)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'add' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mmyprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0madd\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m3\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;36m4\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;31mNameError\u001b[0m: name 'add' is not defined" + ] + } + ], + "source": [ + "myprint(add)(3,4)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "@myprint\n", + "def add(a,b):\n", + " return a+b" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Function add called\n", + "\twith args: (3, 4)\n", + "\t Result --> 7\n" + ] + }, + { + "data": { + "text/plain": [ + "7" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "add(3,4)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### Multiple ways to create and call Decorators\n", + "1. Directly wrap the decorator function around the original function\n", + "2. Decorator expression (using @function)\n", + "3. Null decorators\n", + "4. Class decorators" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Decorators Types\n", + "\n", + "As a prefix that creates a new function with the same name as the base function as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "#@decorator\n", + "def original_function():\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As an explicit operation that returns a new function, possibly with a new name:" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "def original_function():\n", + " pass\n", + "\n", + "#original_function=decorator(original_function)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [], + "source": [ + "## Decorators\n", + "\n", + "def null_decorator(func):\n", + " return func\n", + "\n", + "def greet():\n", + " return 'Hello!'\n", + "\n", + "greet = null_decorator(greet)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Class Decorator" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [], + "source": [ + "class decorator_with_arguments(object):\n", + "\n", + " def __init__(self, arg1, arg2, arg3):\n", + " \"\"\"\n", + " If there are decorator arguments, the function\n", + " to be decorated is not passed to the constructor!\n", + " \"\"\"\n", + " print(\"Inside __init__()\")\n", + " self.arg1 = arg1\n", + " self.arg2 = arg2\n", + " self.arg3 = arg3\n", + "\n", + " def __call__(self, f):\n", + " \"\"\"\n", + " If there are decorator arguments, __call__() is only called\n", + " once, as part of the decoration process! You can only give\n", + " it a single argument, which is the function object.\n", + " \"\"\"\n", + " print(\"Inside __call__()\")\n", + " def wrapped_f(*args):\n", + " print(\"Inside wrapped_f()\")\n", + " print(\"Decorator arguments:\", self.arg1, self.arg2, self.arg3)\n", + " f(*args)\n", + " print(\"After f(*args)\")\n", + " return wrapped_f" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Inside __init__()\n", + "Inside __call__()\n" + ] + } + ], + "source": [ + "@decorator_with_arguments(\"hello\", \"world\", 42)\n", + "def sayHello(a1, a2, a3, a4):\n", + " print('say Hello arguments:', a1, a2, a3, a4)" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "After decoration\n", + "Preparing to call sayHello()\n", + "Inside wrapped_f()\n", + "Decorator arguments: hello world 42\n", + "say Hello arguments: say hello argument list\n", + "After f(*args)\n", + "after first sayHello() call\n", + "Inside wrapped_f()\n", + "Decorator arguments: hello world 42\n", + "say Hello arguments: a different set of arguments\n", + "After f(*args)\n", + "after second sayHello() call\n" + ] + } + ], + "source": [ + "print(\"After decoration\")\n", + "\n", + "print(\"Preparing to call sayHello()\")\n", + "sayHello(\"say\", \"hello\", \"argument\", \"list\")\n", + "print(\"after first sayHello() call\")\n", + "sayHello(\"a\", \"different\", \"set of\", \"arguments\")\n", + "print(\"after second sayHello() call\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Another Example" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "inside my_decorator.__init__()\n", + "inside aFunction()\n", + "Finished decorating aFunction()\n", + "inside my_decorator.__call__()\n" + ] + } + ], + "source": [ + "class my_decorator(object):\n", + "\n", + " def __init__(self, f):\n", + " print(\"inside my_decorator.__init__()\")\n", + " f() # Prove that function definition has completed\n", + "\n", + " def __call__(self):\n", + " print(\"inside my_decorator.__call__()\")\n", + "\n", + "@my_decorator\n", + "def aFunction():\n", + " print(\"inside aFunction()\")\n", + "\n", + "print(\"Finished decorating aFunction()\")\n", + "\n", + "aFunction()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Notes\n", + "\n", + "1. Create class my_decorator\n", + "2. Use decorator expression to decorate aFunction(). At this point, the construst (\\__init__) is called.\n", + "3. Inside the my_decorator.\\__init__ we called the f() which calls aFunction()\n", + "4. Now we call the aFunction() directly and see the .\\__call__() dunder invoked." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Thoughts\n", + "\n", + "You would probably never really call the inner function in the \\__init__ but it is good for the sake of example." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [], + "source": [ + "from datetime import time" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "datetime.time(0, 0)" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "time()" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [], + "source": [ + "t1 = time()" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "00:00:00\n" + ] + } + ], + "source": [ + "print(t1)" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [], + "source": [ + "t2 = time()" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "00:00:00\n" + ] + } + ], + "source": [ + "print(t2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Another example" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Entering func1\n", + "inside func1()\n", + "Exited func1\n", + "Entering func2\n", + "inside func2()\n", + "Exited func2\n" + ] + } + ], + "source": [ + "class entry_exit(object):\n", + "\n", + " def __init__(self, f):\n", + " self.f = f\n", + "\n", + " def __call__(self):\n", + " print(\"Entering\", self.f.__name__)\n", + " self.f()\n", + " print(\"Exited\", self.f.__name__)\n", + "\n", + "@entry_exit\n", + "def func1():\n", + " print(\"inside func1()\")\n", + "\n", + "@entry_exit\n", + "def func2():\n", + " print(\"inside func2()\")\n", + "\n", + "func1()\n", + "func2()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Wraps - functools.wraps" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Backtrack Recursion" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [], + "source": [ + "def permute(list, s):\n", + " if list == 1:\n", + " return s\n", + " else:\n", + " return [ y + x\n", + " for y in permute(1, s)\n", + " for x in permute(list - 1, s)\n", + " ]" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['4', '5', '6']" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "permute(1,[\"4\",\"5\",\"6\"])" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['44', '45', '46', '54', '55', '56', '64', '65', '66']" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "permute(2,[\"4\",\"5\",\"6\"])" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['444',\n", + " '445',\n", + " '446',\n", + " '454',\n", + " '455',\n", + " '456',\n", + " '464',\n", + " '465',\n", + " '466',\n", + " '544',\n", + " '545',\n", + " '546',\n", + " '554',\n", + " '555',\n", + " '556',\n", + " '564',\n", + " '565',\n", + " '566',\n", + " '644',\n", + " '645',\n", + " '646',\n", + " '654',\n", + " '655',\n", + " '656',\n", + " '664',\n", + " '665',\n", + " '666']" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "permute(3,[\"4\",\"5\",\"6\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/Student/kristianjf/lesson04/json_save_mail.py b/Student/kristianjf/lesson04/json_save_mail.py index ac5b7df..b390be8 100644 --- a/Student/kristianjf/lesson04/json_save_mail.py +++ b/Student/kristianjf/lesson04/json_save_mail.py @@ -1,17 +1,41 @@ -from mailroom_6 import * +#!/usr/bin/env python3 + +'''Using metaclass json.save decorator to add ability to save and load json with ease''' +import json import json_save.json_save.json_save.json_save_dec as js -import os +from mailroom_6 import * @js.json_save -class json_dh: +class JsonDh: + '''Create class that uses js.json_save metaclass decorator''' Donors = js.List() def __init__(self, dh): self.Donors = [{'name': donor_object.name, 'donations': donor_object.donations} \ for donor_object in dh.donors.values()] def save(self): - with open('json_out.json', 'w') as outfile: - self.to_json(fp=outfile) + '''Use js method to export attributes to json''' + with open('json_out.json', 'w') as c_outfile: + self.to_json(fp=c_outfile) @classmethod def load(cls, file='json_in.json'): - with open(file, 'r') as infile: - return js.from_json(infile) + '''Use js method to initialize class using json file import''' + with open(file, 'r') as c_infile: + return js.from_json(c_infile) + +if __name__ == '__main__': + # Create instance of json donor handler with populated data structure. + TEST_JSON_DH = JsonDh(dh=PopulateDB()) + # Save JSON export to a file + TEST_JSON_DH.save() + with open('json_out.json') as outfile: + print(f'\nFile Saved\nResults: {json.load(outfile)}\n') + # Create a new file named json_in.json and add a new donor. + with open('json_out.json', 'r') as infile: + PYTHON_IN = json.load(infile) + PYTHON_IN['Donors'].append({'name':'Kristian Francisco', 'donations': [20000]}) + with open('json_in.json', 'w') as outfile: + outfile.write(json.dumps(PYTHON_IN, indent=4, separators=(',', ': '))) + print(f'Added Donor: {PYTHON_IN["Donors"][-1]}\nResults: {PYTHON_IN}\n') + # Load the new json_in.json file using classmethod load + TEST_LOAD_JSON_DH = JsonDh.load(file='json_in.json') + print(f'File Loaded\nResults: {vars(TEST_LOAD_JSON_DH)}\n') diff --git a/Student/kristianjf/lesson05/complicated_example.py b/Student/kristianjf/lesson05/complicated_example.py new file mode 100644 index 0000000..6c16144 --- /dev/null +++ b/Student/kristianjf/lesson05/complicated_example.py @@ -0,0 +1,26 @@ +def main(): + x = 'main' + one() + +def one(): + y = 'one' + two() + +def two(): + z = 'two' + long_loop() + +def long_loop(): + for i in range(2, int(1e03), 5): + for j in range(3, int(1e03), 7): + for k in range(12, int(1e03)): + + z = k / (i % k + j % k) + secret_print(z) + +def secret_print(num): + num + +if __name__ == '__main__': + print(main()) + print("last statement") diff --git a/Student/kristianjf/lesson05/explanation_terminal_output_recursion_pdb.txt b/Student/kristianjf/lesson05/explanation_terminal_output_recursion_pdb.txt new file mode 100644 index 0000000..90534ad --- /dev/null +++ b/Student/kristianjf/lesson05/explanation_terminal_output_recursion_pdb.txt @@ -0,0 +1,109 @@ +What is wrong with our logic? +Why doesn't the function stop calling itself? +What's happening to the value of 'n' as the function gets deeper and deeper into recursion? + +The base case in this recursive function does not consider possible base cases outside of 2. Adding a base case if n < 2 would be required to know if n is a power of 2 + +------ +Terminal Output: +> c:\users\krist\github\sp2018-python220-accelerated\student\kristianjf\lesson05\recursive.py(1)() +-> import sys +(Pdb) n +> c:\users\krist\github\sp2018-python220-accelerated\student\kristianjf\lesson05\recursive.py(3)() +-> def my_fun(n): +(Pdb) n +> c:\users\krist\github\sp2018-python220-accelerated\student\kristianjf\lesson05\recursive.py(9)() +-> if __name__ == '__main__': +(Pdb) n +> c:\users\krist\github\sp2018-python220-accelerated\student\kristianjf\lesson05\recursive.py(10)() +-> n = int(sys.argv[1]) +(Pdb) n +> c:\users\krist\github\sp2018-python220-accelerated\student\kristianjf\lesson05\recursive.py(11)() +-> print(my_fun(n)) +(Pdb) s +--Call-- +> c:\users\krist\github\sp2018-python220-accelerated\student\kristianjf\lesson05\recursive.py(3)my_fun() +-> def my_fun(n): +(Pdb) s +> c:\users\krist\github\sp2018-python220-accelerated\student\kristianjf\lesson05\recursive.py(4)my_fun() +-> if n == 2: +(Pdb) print(n) +12 +(Pdb) s +> c:\users\krist\github\sp2018-python220-accelerated\student\kristianjf\lesson05\recursive.py(7)my_fun() +-> return my_fun(n/2) +(Pdb) s +--Call-- +> c:\users\krist\github\sp2018-python220-accelerated\student\kristianjf\lesson05\recursive.py(3)my_fun() +-> def my_fun(n): +(Pdb) print(n) +6.0 +(Pdb) s +> c:\users\krist\github\sp2018-python220-accelerated\student\kristianjf\lesson05\recursive.py(4)my_fun() +-> if n == 2: + +(Pdb) s +> c:\users\krist\github\sp2018-python220-accelerated\student\kristianjf\lesson05\recursive.py(7)my_fun() +-> return my_fun(n/2) +(Pdb) print(n) +6.0 +(Pdb) s +--Call-- +> c:\users\krist\github\sp2018-python220-accelerated\student\kristianjf\lesson05\recursive.py(3)my_fun() +-> def my_fun(n): +(Pdb) print(n) +3.0 +(Pdb) s +> c:\users\krist\github\sp2018-python220-accelerated\student\kristianjf\lesson05\recursive.py(4)my_fun() +-> if n == 2: +(Pdb) s +> c:\users\krist\github\sp2018-python220-accelerated\student\kristianjf\lesson05\recursive.py(7)my_fun() +-> return my_fun(n/2) +(Pdb) s +--Call-- +> c:\users\krist\github\sp2018-python220-accelerated\student\kristianjf\lesson05\recursive.py(3)my_fun() +-> def my_fun(n): +(Pdb) s +> c:\users\krist\github\sp2018-python220-accelerated\student\kristianjf\lesson05\recursive.py(4)my_fun() +-> if n == 2: +(Pdb) s +> c:\users\krist\github\sp2018-python220-accelerated\student\kristianjf\lesson05\recursive.py(7)my_fun() +-> return my_fun(n/2) +(Pdb) print(n) +1.5 +(Pdb) n +--KeyboardInterrupt-- +(Pdb) n +Traceback (most recent call last): + File "C:\Program Files\Python36\lib\pdb.py", line 1667, in main + pdb._runscript(mainpyfile) + + File "C:\Program Files\Python36\lib\pdb.py", line 1548, in _runscript + self.run(statement) + File "C:\Program Files\Python36\lib\bdb.py", line 434, in run + exec(cmd, globals, locals) + File "", line 1, in + File "c:\users\krist\github\sp2018-python220-accelerated\student\kristianjf\lesson05\recursive.py", line 11, in + print(my_fun(n)) + File "c:\users\krist\github\sp2018-python220-accelerated\student\kristianjf\lesson05\recursive.py", line 7, in my_fun + return my_fun(n/2) + File "c:\users\krist\github\sp2018-python220-accelerated\student\kristianjf\lesson05\recursive.py", line 7, in my_fun + return my_fun(n/2) + File "c:\users\krist\github\sp2018-python220-accelerated\student\kristianjf\lesson05\recursive.py", line 7, in my_fun + return my_fun(n/2) + [Previous line repeated 980 more times] + File "c:\users\krist\github\sp2018-python220-accelerated\student\kristianjf\lesson05\recursive.py", line 3, in my_fun + def my_fun(n): + File "C:\Program Files\Python36\lib\bdb.py", line 53, in trace_dispatch + return self.dispatch_call(frame, arg) + File "C:\Program Files\Python36\lib\bdb.py", line 79, in dispatch_call + if not (self.stop_here(frame) or self.break_anywhere(frame)): + File "C:\Program Files\Python36\lib\bdb.py", line 176, in break_anywhere + return self.canonic(frame.f_code.co_filename) in self.breaks + File "C:\Program Files\Python36\lib\bdb.py", line 32, in canonic + if filename == "<" + filename[1:-1] + ">": +RecursionError: maximum recursion depth exceeded in comparison +Uncaught exception. Entering post mortem debugging +Running 'cont' or 'step' will restart the program +> c:\program files\python36\lib\bdb.py(32)canonic() +-> if filename == "<" + filename[1:-1] + ">": diff --git a/Student/kristianjf/lesson05/logging_assignment.py b/Student/kristianjf/lesson05/logging_assignment.py new file mode 100755 index 0000000..a02b168 --- /dev/null +++ b/Student/kristianjf/lesson05/logging_assignment.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +import logging, logging.handlers + +'''To complete this assignment, modify simple.py to satisfy the following goals: + +You want ALL log messages logged to the console. +The format of these messages should include the current time. +You want WARNING and higher messages logged to a file named { todays-date }.log. + The format of these messages should include the current time. +You want ERROR and higher messages logged to a syslog server. + The syslog server will be appending its own time stamps to the messages that it receives, + so DO NOT include the current time in the format of the log messages that you send + to the server. +''' + +format_console = "%(asctime)s %(filename)s:%(lineno)-3d %(levelname)s %(message)s" +format_syslog = "%(filename)s:%(lineno)-3d %(levelname)s %(message)s" +formatter_console = logging.Formatter(format_console) +formatter_syslog = logging.Formatter(format_syslog) + +file_handler = logging.FileHandler('mylog.log') +file_handler.setLevel(logging.WARNING) +file_handler.setFormatter(formatter_console) + +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter_console) + +syslog_handler = logging.handlers.DatagramHandler('127.0.0.1', 514) +syslog_handler.setLevel(logging.ERROR) +syslog_handler.setFormatter(formatter_syslog) + +logger = logging.getLogger() +logger.setLevel(logging.DEBUG) +logger.addHandler(file_handler) +logger.addHandler(console_handler) +logger.addHandler(syslog_handler) + +def my_fun(n): + for i in range(0, n): + logging.debug(i) + if i == 50: + logging.warning("The value of i is 50.") + try: + i / (50 - i) + except ZeroDivisionError: + logging.error("Tried to divide by zero. Var i was {}. Recovered gracefully.".format(i)) + +if __name__ == "__main__": + my_fun(100) diff --git a/Student/kristianjf/lesson05/pysyslog.py b/Student/kristianjf/lesson05/pysyslog.py new file mode 100755 index 0000000..25c129a --- /dev/null +++ b/Student/kristianjf/lesson05/pysyslog.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 + +## Tiny Syslog Server in Python. +## +## This is a tiny syslog server that is able to receive UDP based syslog +## entries on a specified port and save them to a file. +## That's it... it does nothing else... +## There are a few configuration parameters. + +LOG_FILE = 'yourlogfile.log' +HOST = '0.0.0.0' +TCP_PORT = 1514 +UDP_PORT = 514 + +# +# NO USER SERVICEABLE PARTS BELOW HERE... +# + +import logging +import time +import threading +import socketserver + +listening = False + +logging.basicConfig(level=logging.INFO, format='%(message)s', datefmt='', filename=LOG_FILE, filemode='a') + +class SyslogUDPHandler(socketserver.BaseRequestHandler): + + def handle(self): + # data = bytes.decode(self.request[0].strip()) + data = self.request[0].strip() + # socket = self.request[1] + print( "%s : " % self.client_address[0], str(data)) + logging.info(str(data)) + +class SyslogTCPHandler(socketserver.BaseRequestHandler): + + End = '\n' + def join_data(self, total_data): + final_data = ''.join(total_data) + for data in final_data.split(self.End): + print( "%s : " % self.client_address[0], str(data)) + logging.info(str(data)) + + def handle(self): + total_data = [] + while listening: + data = self.request.recv(8192).strip() + if not data: break + if self.End in data: + split_index = data.rfind(self.End) + total_data.append(data[:split_index]) + self.join_data(total_data) + del total_data[:] + total_data.append(data[split_index + 1:]) + else: + total_data.append(data) + if len(total_data) > 0: + self.join_data(total_data) + # logging.info(str(data)) + +if __name__ == "__main__": + listening = True + try: + # UDP server + udpServer = socketserver.UDPServer((HOST, UDP_PORT), SyslogUDPHandler) + udpThread = threading.Thread(target=udpServer.serve_forever) + udpThread.daemon = True + udpThread.start() + # udpServer.serve_forever(poll_interval=0.5) + + # TCP server + tcpServer = socketserver.TCPServer((HOST, TCP_PORT), SyslogTCPHandler) + tcpThread = threading.Thread(target=tcpServer.serve_forever) + tcpThread.daemon = True + tcpThread.start() + + while True: + time.sleep(1) + # tcpServer.serve_forever(poll_interval=0.5) + except (IOError, SystemExit): + raise + except KeyboardInterrupt: + listening = False + udpServer.shutdown() + udpServer.server_close() + tcpServer.shutdown() + tcpServer.server_close() + print ("Crtl+C Pressed. Shutting down.") diff --git a/Student/kristianjf/lesson05/recursive.py b/Student/kristianjf/lesson05/recursive.py new file mode 100644 index 0000000..6280579 --- /dev/null +++ b/Student/kristianjf/lesson05/recursive.py @@ -0,0 +1,11 @@ +import sys + +def my_fun(n): + if n == 2: + return True + + return my_fun(n/2) + +if __name__ == '__main__': + n = int(sys.argv[1]) + print(my_fun(n)) diff --git a/Student/kristianjf/lesson06/roman-numerals-exercise/.gitignore b/Student/kristianjf/lesson06/roman-numerals-exercise/.gitignore new file mode 100644 index 0000000..004a8d7 --- /dev/null +++ b/Student/kristianjf/lesson06/roman-numerals-exercise/.gitignore @@ -0,0 +1,2 @@ +*.pyc +.coverage diff --git a/Student/kristianjf/lesson06/roman-numerals-exercise/README.md b/Student/kristianjf/lesson06/roman-numerals-exercise/README.md new file mode 100644 index 0000000..a6863ff --- /dev/null +++ b/Student/kristianjf/lesson06/roman-numerals-exercise/README.md @@ -0,0 +1,42 @@ +# Roman Numerals, Advanced Testing Exercise + +Before the invention and wide-spread use of the number "zero", many cultures used different ways to represent large numbers. This exercise is based on the Roman method of representing large numbers, known as "Roman numerals." + +In the Roman numeral system, certain characters represent certain numbers. The following table gives the number value of the Roman numerals that we will use in this exercise: + +* I: 1 +* V: 5 +* X: 10 +* L: 50 +* C: 100 +* D: 500 +* M: 1000 + +A composite number can be produced by listing several characters, from largest-valued to smallest-valued and adding their values. For example: + +* LX: 60 +* LXV: 65 +* MMMD: 3500 + +Additionally, if a smaller-valued numeral _comes before_ a larger-valued numeral, then that value is subtracted from the total value, rather than added. For example: + +* IV: (5 - 1): 4 +* MMIV: 1000 + 1000 + (5 - 1): 2004 +* XC: (100 - 10): 90 + +There's a version of the Roman numeral system where _any_ smaller-valued numeral which comes before a larger-valued numeral is subtracted from the total, but we won't use that version of numerals. + +This repository includes a `RomanToInt` class which can convert a Roman numeral string into an integer. The class works on Roman numeral strings with a value up to 3999. You can use this class at the command line, using the included `main.py`. For example: `python main.py IMMMM`. + +## Your Goals + +1. Write a comprehensive set of tests into `test.py`. +2. All of your tests should pass, using: `python -m unittest test.py`. +3. Running `coverage run --source=roman_numerals -m unittest test.py; coverage report` should give a coverage of 100%. +4. Satisfy the linter such that `pylint roman_numerals` and `flake8 roman_numerals` show no errors. + +## Additional Comments + +Feel free to satisfy the linter through any combination of making improvements to the files and/or creating a pylint configuration file which ignores particular errors. If you create a custom configuration file, then be sure to `git include` it in your repository. + +For the purposes of this exercise, the code may include some bad style. Feel free to refactor the code if you see opportunities for improvement. diff --git a/Student/kristianjf/lesson06/roman-numerals-exercise/main.py b/Student/kristianjf/lesson06/roman-numerals-exercise/main.py new file mode 100644 index 0000000..2245d78 --- /dev/null +++ b/Student/kristianjf/lesson06/roman-numerals-exercise/main.py @@ -0,0 +1,10 @@ +import sys + +from roman_numerals.roman_to_int import RomanToInt + + +if __name__ == "__main__": + s = sys.argv[1] + + print("The Roman numerals {} are {} in decimal.".format(s, RomanToInt.convert(s))) + diff --git a/Student/kristianjf/lesson06/roman-numerals-exercise/roman_numerals/__init__.py b/Student/kristianjf/lesson06/roman-numerals-exercise/roman_numerals/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Student/kristianjf/lesson06/roman-numerals-exercise/roman_numerals/roman_to_int.py b/Student/kristianjf/lesson06/roman-numerals-exercise/roman_numerals/roman_to_int.py new file mode 100644 index 0000000..df0a44d --- /dev/null +++ b/Student/kristianjf/lesson06/roman-numerals-exercise/roman_numerals/roman_to_int.py @@ -0,0 +1,29 @@ +##!/usr/bin/env python3 +'''Convert Roman Numerals to Integers''' + + +class RomanToInt(object): + '''Class to manage roman numeral to integer conversion''' + @staticmethod + def value_of(roman_num): + '''Equate roman numeral to integer''' + roman_dict = {'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, + 'D': 500, 'M': 1000} + if roman_num in roman_dict: + return roman_dict.get(roman_num) + else: + raise ValueError( + "Provided character must be one of: I V X L C D M.") + + @classmethod + def convert(cls, roman_num_in): + '''Logic to convert roman numeral to integer''' + result = 0 + for i, roman_num in enumerate(roman_num_in): + if (i + 1) < len(roman_num_in) and cls.value_of(roman_num) < \ + cls.value_of(roman_num_in[i + 1]): + result -= cls.value_of(roman_num) + else: + result += cls.value_of(roman_num) + + return result diff --git a/Student/kristianjf/lesson06/roman-numerals-exercise/test.py b/Student/kristianjf/lesson06/roman-numerals-exercise/test.py new file mode 100644 index 0000000..39bb243 --- /dev/null +++ b/Student/kristianjf/lesson06/roman-numerals-exercise/test.py @@ -0,0 +1,22 @@ +import unittest + +from roman_numerals.roman_to_int import RomanToInt + + +class TestRomanToInteger(unittest.TestCase): + + roman_dict = {'I':1, 'V':5, 'X':10, 'L':50, 'C':100, 'D':500, 'M':1000} + def test_roman_numerals(self): + for roman_num, num in self.roman_dict.items(): + self.assertEqual(RomanToInt.convert(roman_num), num) + + def test_small_large(self): + for i in range(len(self.roman_dict)-1): + roman_tuple = sorted(self.roman_dict.items(), key=lambda x: x[1]) + two_roman = roman_tuple[i][0] + roman_tuple[i+1][0] + two_num = (roman_tuple[i][1], roman_tuple[i+1][1]) + self.assertEqual(RomanToInt.convert(two_roman), (lambda x: x[1] - x[0])(x=two_num)) + + def test_errors(self): + with self.assertRaises(ValueError): + RomanToInt.convert('A') diff --git a/Student/kristianjf/lesson07/mailroom_6.py b/Student/kristianjf/lesson07/mailroom_6.py new file mode 100644 index 0000000..6ec73e3 --- /dev/null +++ b/Student/kristianjf/lesson07/mailroom_6.py @@ -0,0 +1,354 @@ +#!/usr/bin/env python3 +import sys +import logging +from datetime import datetime +from functools import reduce +from peewee import * +''' +Goal: +You work in the mail room at a local charity. Part of your job is to write \ +incredibly boring, repetitive emails thanking your donors for their generous \ +gifts. You are tired of doing this over and over again, so you’ve decided to \ +let Python help you out of a jam and do your work for you. +''' +''' +It should have a data structure that holds a list of your donors and a history of \ +the amounts they have donated. This structure should be populated at first with at \ +least five donors, with between 1 and 3 donations each. +''' + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +logger.info('Connecting to donor.db') +database = SqliteDatabase('donor.db') +database.connect() +database.execute_sql('PRAGMA foreign_keys = ON;') +logger.info(f'Connected to {database.database}') + +class BaseModel(Model): + class Meta: + database = database + +class DonorTable(BaseModel): + """ + This class defines Donor, which maintains details of someone + for whom we want to track donations. + """ + fld_name = CharField(primary_key=True, max_length=30) + +class DonationTable(BaseModel): + """ + This class defines Donation, which maintains details of transactions + of someone for me whom we collect donations. + """ + fld_timestamp = DateTimeField(default=datetime.now) + fld_donor = ForeignKeyField(DonorTable, null=False) + fld_donations = IntegerField() + +database = SqliteDatabase('donor.db') +'''Create Donation Table''' +logger.info('Creating Donor and Donation Table') +database.create_tables([DonorTable, DonationTable]) + +class Donor(): + database = SqliteDatabase('donor.db') + def __init__(self, name, donations): + self._name = name.title() + # self._normalized_name = '' + # self._normalized_name = name.title() + self._donations = donations if isinstance(donations, list) else [donations] + '''Add Donor Record to Table''' + + try: + database.connect() + database.execute_sql('PRAGMA foreign_keys = ON;') + if not self._name in DonorTable: + with database.transaction(): + donor_record = DonorTable.create(fld_name=self._name) + # donor_record.save() + logger.info(f'Added Donor to Donor Table: {DonorTable.get(DonorTable.fld_name == self._name)}') + with database.transaction(): + for donation in self._donations: + donation_record = DonationTable.create(fld_donor=self._name,\ + fld_donations=donation) + logger.info(f'Adding Donation to Donor Table: {self._name, donation}') + # donation_record.save() + # logger.info(f'Added Donations to Donor Table: {self._name, DonationTable.get(DonationTable.fld_donor == self._name)}') + logger.info(f'Added Donations to Donor Table: {self._name, [x.fld_donations for x in DonationTable.select().where(DonationTable.fld_donor == self._name)]}') + except Exception as e: + logger.info(f'Error creating = {name, donations}') + logger.info(e) + finally: + logger.info('database closes') + database.close() + + def __lt__(self, other): + return sum(self.donations) < sum(other.donations) + @property + def name(self): + return self._name + @property + def donations(self): + donation_list = [] + for donor in DonationTable.select().where(DonationTable.fld_donor == self._name): + donation_list.append(donor.fld_donations) + return donation_list + # return self._donations + @property + def normalized_name(self): + return self._name + def donate(self, new_donation): + self.donations.append(new_donation) + def metrics(self): + num_d = len(self.donations) + total_d = sum(self.donations) + avg_d = total_d / num_d + return num_d, total_d, avg_d + + def send_letter(self): + ''' + Try to use a dict and the .format() method to do the letter as one big \ + template rather than building up a big string in parts. + In this version, add a function (and a menu item to invoke it), that goes \ + through all the donors in your donor data structure, generates a thank you \ + letter, and writes it to disk as a text file. + ''' + filename = '_'.join(self.name.split())+'.txt' + with open(filename, 'w') as outfile: + outfile.write(self.output_string().format(name=self.name, donation=self.donations)) + print(f'Writing: {filename}') + + def output_string(self): + 'Output string for letters' + format_string = 'Dear {name},\n\nThank you for your generous donation of ' \ + '${donation}. Please send us more money at your earliest ' \ + 'convenience.' + return format_string + +class DonorHandler: + def __init__(self): + self.donors = {} + def add_donor(self, donor_object): + self.donors[donor_object.normalized_name] = donor_object + def dnr_challenge(self, factor=1, **kwargs): + new_dh = DonorHandler() + for donor, donor_object in self.donors.items(): + '''Add a new feature to Mailroom using filter so that donations either above + or below a specified dollar amount are included in the map operations of #1 above. + You can do this by adding min_donation and max_donation optional keyword parameters + to your challenge function. You’ll want to filter the donations before + passing them to map.''' + filter_donations = self.dnr_filter(\ + donor_object.donations, kwargs['min_donation'], kwargs['max_donation']) + + '''Add a new feature to Mailroom using map so that each donation on record can be + doubled, tripled or indeed multiplied by any arbitrary factor based on the whims + of philanthropists who would like to support our cause. + This will require a new function (or method in your donor database class) called + challenge(factor) that takes a multiplier (factor), and multiplies all the + donations of all the donors by the factor. The function returns a NEW donor database, + with the new data.''' + map_donations_f = list(map(lambda x, y: x*y, filter_donations,\ + len(filter_donations)*[factor])) + + new_d = Donor(donor+' Projection', map_donations_f) + new_dh.add_donor(new_d) + return new_dh + def dnr_filter(self, lst, min_donation=0, max_donation=10000000): + lst = list(filter(lambda x: (x > min_donation) and (x < max_donation), lst)) + return lst + + def dnr_projections(self, donor, **kwargs): + ''' + For instance, based on donations in the current database, show them (a) what their total + contribution would come to in dollars if they were to double contributions under $100. + And then (b) show them what their total contribution would come to if they were to + triple contributions over $50. + ''' + dnr_proj = self.dnr_challenge(**kwargs) + return reduce(lambda x, y: x+y, dnr_proj.donors[donor].donations) + + def thank_you(self, full_name='', donation_amount=int(), **kwargs): + ''' + If the user (you) selects ‘Send a Thank You’, prompt for a Full Name. + If the user types a name not in the list, add that name to the data structure and use it. + If the user types a name in the list, use it. + Once a name has been selected, prompt for a donation amount. + Turn the amount into a number – it is OK at this point for the program to crash if \ + someone types a bogus amount. + Once an amount has been given, add that amount to the donation history of the selected user. + Finally, use string formatting to compose an email thanking the donor for their generous \ + donation. Print the email to the terminal and return to the original prompt. + ''' + ''' + Collect, Format and Validate Full name + ''' + while not full_name: + full_name_input = input('Please enter full name of the recipient: ').split() + full_name = self.name_response_valid(full_name_input) + ''' + Process Full Name + ''' + ds_names = [name for name in self.donors] + + if full_name in ds_names: + print(f'Found {full_name} in data_structure') + while not donation_amount: + donation_amount = int(input('Please enter a donation amount: ')) + self.donors[full_name].donate(donation_amount) + print(self.donors[full_name].output_string().format(name=full_name, \ + donation=donation_amount)) + else: + while not donation_amount: + donation_amount = int(input('Please enter a donation amount: ')) + self.add_donor(Donor(full_name, donation_amount)) + print(self.donors[full_name].output_string().format(name=full_name, \ + donation=donation_amount)) + return self.donors[full_name].output_string().format(name=full_name, \ + donation=donation_amount) + + def list_donors(self, **kwargs): + ''' + If the user types ‘list’, show them a list of the donor names and re-prompt + ''' + for name in self.donors: + print(name) + return [name for name in self.donors] + + def create_a_report(self, **kwargs): + ''' + Creating a Report + If the user (you) selected “Create a Report”, print a list of your donors, \ + sorted by total historical donation amount. + ''' + print('Menu: Create a Report') + top_row = ('Donor Name', 'Total Given', 'Num Gifts', 'Average Gift') + format_header = '{:<30} | {:<15} | {:<10} | {:<15}\n' + format_data = '{:<30} $ {:<15.2f} {:<10} $ {:<15.2f}\n' + format_string = format_header.format(*top_row) + for donor in sorted(self.donors.values(), reverse=True): + name = donor.name + num_gifts, total_given, avg_gift = donor.metrics() + format_string += format_data.format(name, total_given, num_gifts, avg_gift) + print(format_string) + return format_string + + def send_letters(self, **kwargs): + ''' + Try to use a dict and the .format() method to do the letter as one big \ + template rather than building up a big string in parts. + In this version, add a function (and a menu item to invoke it), that goes \ + through all the donors in your donor data structure, generates a thank you \ + letter, and writes it to disk as a text file. + ''' + for donor in self.donors.values(): + donor.send_letter() + + def run_projection(self, full_name='', max_donation_amount=int(), factor=int(), **kwargs): + 'Select from list of Donors to Run Projections' + # donor_list_dict = {full_name: self.dnr_projections for donor in self.donors} + donor_list_dict = {donor: kwargs.update({'full_name': donor}) for donor in self.donors} + menu(donor_list_dict) + 'Collect Donor Name, Max Donation Amount, Factor' + # while not full_name: + # full_name_input = input('Please enter full name of the recipient: ').split() + # full_name = self.name_response_valid(full_name_input) + while not max_donation_amount: + max_donation_amount = int(input('Please enter a max donation amount: ')) + while not factor: + factor = int(input('Please enter a factor: ')) + donation_amount = self.dnr_projections(donor=full_name, min_donation=1, \ + max_donation=max_donation_amount, factor=factor, **kwargs) + print(self.donors[full_name].output_string().format(name=full_name, \ + donation=donation_amount)) + return self.donors[full_name].output_string().format(name=full_name, \ + donation=donation_amount) + + #Name Validation + def name_response_valid(self, full_name): + 'Validate and Return Name Input' + full_name_cap = '' + try: + for name in full_name: + assert name.isalpha() + full_name_cap += f'{name.capitalize()} ' + full_name = full_name_cap.strip() + except AssertionError: + print('Invalid Name. Found non-alphabetic characters') + else: + return full_name + +#Function to Process Menu Options +def menu(options_dict, **kwargs): + ''' + The script should prompt the user (you) to choose from a menu of 3 actions: \ + “Send a Thank You”, “Create a Report” or “quit”) + ''' + options = [option[0:2] for option in enumerate(options_dict.keys(), 1)] + while True: + print('Please select a number from the list of the following options: \n') + for option in options: + print(option) + response = menu_response_valid(options) + response_selection = options[response-1][1] + options_dict[response_selection](**kwargs) + +#Main Menu +def main_menu(**kwargs): + 'Create Main Menu' + program_options_dict = {'Send a Thank You': thank_you_menu, \ + 'Create a Report': kwargs['donor_handler'].create_a_report, \ + 'Send Letters to Everyone': kwargs['donor_handler'].send_letters, \ + 'Run Donor Projections': kwargs['donor_handler'].run_projection, \ + 'quit': quit_menu} + menu(program_options_dict, **kwargs) + +#Thank You Menu +def thank_you_menu(**kwargs): + ''' + Sending a Thank You + If the user (you) selects ‘Send a Thank You’, prompt for a Full Name. + If the user types ‘list’, show them a list of the donor names and re-prompt + ''' + program_options_ty = {'Send a Thank You': kwargs['donor_handler'].thank_you, \ + 'Action: List': kwargs['donor_handler'].list_donors, \ + 'quit': main_menu} + menu(program_options_ty, **kwargs) + +def quit_menu(**kwargs): + sys.exit() + +#Menu Option Validation +def menu_response_valid(options): + 'Validate Menu Options' + try: + response = int(input()) + assert response in [option[0] for option in options] + except ValueError: + print('Non-integer value entered. Please try again.') + except AssertionError: + print('This number does not exist in the list of options. Please try again.') + else: + return response + #Name Validation + +def PopulateDB(): + 'Create Donors' + jb = Donor('Jeff Bezos', [1, 5, 10]) + bg = Donor('Bill Gates', [10000]) + sj = Donor('Steve Jobs', [20, 50, 100]) + 'Create Donors Handle' + dh = DonorHandler() + dh.add_donor(jb) + dh.add_donor(bg) + dh.add_donor(sj) + return dh + +def main(): + dh = PopulateDB() + 'Call Main Menu' + main_menu(donor_handler=dh) + +if __name__ == '__main__': + main() diff --git a/Student/kristianjf/lesson07/test_mailroom_6.py b/Student/kristianjf/lesson07/test_mailroom_6.py new file mode 100644 index 0000000..bbc7441 --- /dev/null +++ b/Student/kristianjf/lesson07/test_mailroom_6.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 + +import mailroom_6 as m6 + +def clear_donations(donor_name): + m6.DonationTable.delete().where(m6.DonationTable.fld_donor == donor_name).execute() + +def test_donor(): + clear_donations('Kristian Francisco') + kf = m6.Donor('Kristian Francisco', [1000, 100]) + test_metrics = kf.metrics() + assert kf.name == 'Kristian Francisco' + assert test_metrics[0] == 2 + assert test_metrics[1] == 1100 + assert test_metrics[2] == 550.0 + clear_donations('Kristian Francisco') + +def test_donor_handle(): + clear_donations('Kristian Francisco') + kf = m6.Donor('Kristian Francisco', [1000, 100]) + dh = m6.DonorHandler() + dh.add_donor(kf) + assert 'Kristian Francisco' in dh.donors + assert dh.donors['Kristian Francisco'].donations == [1000, 100] + clear_donations('Kristian Francisco') + +def test_donor_challenge(): + clear_donations('Kristian Francisco') + clear_donations('Kristian Francisco Projection') + kf = m6.Donor('Kristian Francisco', [5, 1000, 100]) + dh = m6.DonorHandler() + dh.add_donor(kf) + dh2 = dh.dnr_challenge(factor=4, min_donation=10, max_donation=1000) + print(dh2.donors['Kristian Francisco Projection'].donations) + assert dh2.donors['Kristian Francisco Projection'].donations == [400] + # assert False + clear_donations('Kristian Francisco') + clear_donations('Kristian Francisco Projection') + +def test_donor_projection(): + clear_donations('Kristian Francisco') + clear_donations('Kristian Francisco Projection') + kf = m6.Donor('Kristian Francisco', [20, 1000, 100]) + dh = m6.DonorHandler() + dh.add_donor(kf) + prj_1 = dh.dnr_projections('Kristian Francisco Projection', factor=4, min_donation=10, max_donation=1000) + assert prj_1 == 480 + clear_donations('Kristian Francisco') + clear_donations('Kristian Francisco Projection') diff --git a/Student/kristianjf/lesson08/nosql_activity/src/learn_data.py b/Student/kristianjf/lesson08/nosql_activity/src/learn_data.py new file mode 100644 index 0000000..efe6c21 --- /dev/null +++ b/Student/kristianjf/lesson08/nosql_activity/src/learn_data.py @@ -0,0 +1,49 @@ +""" + Data for database demonstrations +""" + + +def get_furniture_data(): + """ + demonstration data + """ + + furniture_data = [ + { + 'product': 'Red couch', + 'description': 'Leather low back', + 'monthly_rental_cost': 12.99, + 'in_stock_quantity': 10 + }, + { + 'product': 'Blue couch', + 'description': 'Cloth high back', + 'monthly_rental_cost': 9.99, + 'in_stock_quantity': 3 + }, + { + 'product': 'Coffee table', + 'description': 'Plastic', + 'monthly_rental_cost': 2.50, + 'in_stock_quantity': 25 + }, + { + 'product': 'Red couch', + 'description': 'Leather high back', + 'monthly_rental_cost': 15.99, + 'in_stock_quantity': 17 + }, + { + 'product': 'Blue recliner', + 'description': 'Leather high back', + 'monthly_rental_cost': 19.99, + 'in_stock_quantity': 6 + }, + { + 'product': 'Chair', + 'description': 'Plastic', + 'monthly_rental_cost': 1.00, + 'in_stock_quantity': 45 + } + ] + return furniture_data diff --git a/Student/kristianjf/lesson08/nosql_activity/src/learnnosql_activity.py b/Student/kristianjf/lesson08/nosql_activity/src/learnnosql_activity.py new file mode 100644 index 0000000..35059e6 --- /dev/null +++ b/Student/kristianjf/lesson08/nosql_activity/src/learnnosql_activity.py @@ -0,0 +1,39 @@ +""" + +Integrated example for nosql databases + +""" + +import learn_data +import mongodb_script +import redis_script +import neo4j_script +import simple_script_activity +import utilities + + +def showoff_databases_activity(): + """ + Here we illustrate basic interaction with nosql databases + """ + + log = utilities.configure_logger('default', '../logs/nosql_dev.log') + + log.info("Mongodb example to use data from Furniture module, so get it") + furniture = learn_data.get_furniture_data() + + mongodb_script.run_example_activity(furniture) + + log.info("Other databases use data embedded in the modules") + + redis_script.run_example_activity() + neo4j_script.run_example_activity() + simple_script_activity.run_pickle_activity() + + +if __name__ == '__main__': + """ + orchestrate nosql examples + """ + + showoff_databases_activity() diff --git a/Student/kristianjf/lesson08/nosql_activity/src/login_database.py b/Student/kristianjf/lesson08/nosql_activity/src/login_database.py new file mode 100644 index 0000000..0a06b14 --- /dev/null +++ b/Student/kristianjf/lesson08/nosql_activity/src/login_database.py @@ -0,0 +1,86 @@ +""" + module that will login to the various demonstration databases consistently +""" + +import configparser +from pathlib import Path +import pymongo +import redis +from neo4j.v1 import GraphDatabase, basic_auth + +import utilities + +log = utilities.configure_logger('default', '../logs/login_databases_dev.log') +config_file = Path(__file__).parent.parent / '.config/config.ini' +config = configparser.ConfigParser() + + +def login_mongodb_cloud(): + """ + connect to mongodb and login + """ + + log.info('Here is where we use the connect to mongodb.') + log.info('Note use of f string to embed the user & password (from the tuple).') + try: + config.read(config_file) + user = config["mongodb_cloud"]["user"] + pw = config["mongodb_cloud"]["pw"] + connection = config["mongodb_cloud"]["connection"] + + except Exception as e: + print(f'error: {e}') + + # client = pymongo.MongoClient(f'mongodb://{user}:{pw}' + # '@cluster0-shard-00-00-wphqo.mongodb.net:27017,' + # 'cluster0-shard-00-01-wphqo.mongodb.net:27017,' + # 'cluster0-shard-00-02-wphqo.mongodb.net:27017/test' + # '?ssl=true&replicaSet=Cluster0-shard-0&authSource=admin') + client = pymongo.MongoClient(connection.format(user=user, pw=pw)) + + return client + + +def login_redis_cloud(): + """ + connect to redis and login + """ + try: + config.read(config_file) + host = config["redis_cloud"]["host"] + port = config["redis_cloud"]["port"] + pw = config["redis_cloud"]["pw"] + + + except Exception as e: + print(f'error: {e}') + + log.info('Here is where we use the connect to redis.') + + try: + r = redis.StrictRedis(host=host, port=port, password=pw, decode_responses=True) + + except Exception as e: + print(f'error: {e}') + + return r + + +def login_neo4j_cloud(): + """ + connect to neo4j and login + + """ + + log.info('Here is where we use the connect to neo4j.') + log.info('') + + config.read(config_file) + + graphenedb_user = config["neo4j_cloud"]["user"] + graphenedb_pass = config["neo4j_cloud"]["pw"] + graphenedb_url = 'bolt://hobby-opmhmhgpkdehgbkejbochpal.dbs.graphenedb.com:24786' + driver = GraphDatabase.driver(graphenedb_url, + auth=basic_auth(graphenedb_user, graphenedb_pass)) + + return driver diff --git a/Student/kristianjf/lesson08/nosql_activity/src/mongodb_script.py b/Student/kristianjf/lesson08/nosql_activity/src/mongodb_script.py new file mode 100644 index 0000000..269989f --- /dev/null +++ b/Student/kristianjf/lesson08/nosql_activity/src/mongodb_script.py @@ -0,0 +1,126 @@ +""" + mongodb example +""" + +import pprint +import login_database +import utilities + +log = utilities.configure_logger('default', '../logs/mongodb_script.log') + + +def run_example(furniture_items): + """ + mongodb data manipulation + """ + + with login_database.login_mongodb_cloud() as client: + log.info('Step 1: We are going to use a database called dev') + log.info('But if it doesnt exist mongodb creates it') + db = client['dev'] + + log.info('And in that database use a collection called furniture') + log.info('If it doesnt exist mongodb creates it') + + furniture = db['furniture'] + + log.info('Step 2: Now we add data from the dictionary above') + furniture.insert_many(furniture_items) + + log.info('Step 3: Find the products that are described as plastic') + query = {'description': 'Plastic'} + results = furniture.find_one(query) + + log.info('Step 4: Print the plastic products') + print('Plastic products') + pprint.pprint(results) + + log.info('Step 5: Delete the blue couch (actually deletes all blue couches)') + furniture.remove({"product": {"$eq": "Blue couch"}}) + + log.info('Step 6: Check it is deleted with a query and print') + query = {'product': 'Blue couch'} + results = furniture.find_one(query) + print('The blue couch is deleted, print should show none:') + pprint.pprint(results) + + log.info( + 'Step 7: Find multiple documents, iterate though the results and print') + + cursor = furniture.find({'monthly_rental_cost': {'$gte': 15.00}}).sort('monthly_rental_cost', 1) + print('Results of search') + log.info('Notice how we parse out the data from the document') + + for doc in cursor: + print(f"Cost: {doc['monthly_rental_cost']} product name: {doc['product']} Stock: {doc['in_stock_quantity']}") + + log.info('Step 8: Delete the collection so we can start over') + db.drop_collection('furniture') + +def run_example_activity(furniture_items): + """ + mongodb data manipulation -- activity 8 + """ + with login_database.login_mongodb_cloud() as client: + log.info('Step 1: We are going to use a database called dev') + log.info('But if it doesnt exist mongodb creates it') + db = client['dev'] + + log.info('And in that database use a collection called furniture') + log.info('If it doesnt exist mongodb creates it') + + furniture = db['furniture'] + + '''Add some extra furniture items for mongodb. + And while you're doing that, separate the product field in to 2 fields; + one called product type, one called color. + Start by amending the data, + then change the Mongodb program to store and retrieve using these new values. + ''' + log.info('Amending furuniture items with "some extra furniture items"') + add_furniture_items = [ + { + 'product': 'Red Chair', + 'description': 'Plastic', + 'monthly_rental_cost': 2.00, + 'in_stock_quantity': 10 + }, + { + 'product': 'Blue Chair', + 'description': 'Plastic', + 'monthly_rental_cost': 3.00, + 'in_stock_quantity': 25 + } + ] + furniture_items.extend(add_furniture_items) + + log.info('Separating the product field into 2 fields; product type and color.') + colors = ['Red', 'Blue', 'White'] + for entry in furniture_items: + for word in entry['product'].split(): + if word in colors: + entry['color'] = word + break + else: + entry['color'] = 'Undefined' + entry['product type'] = ' '.join([x for x in entry['product'].split() \ + if x not in colors]).capitalize() + del entry['product'] + + log.info('Step 2: Now we add data from the dictionary above') + furniture.insert_many(furniture_items) + + log.info('Step 3.1: Retrieve and print just the red products.') + query = {'color': 'Red'} + results = furniture.find(query) + for entry in results: + pprint.pprint(entry) + + log.info('Step 3.2: Retrieve and print just the couches.') + query = {'product type': 'Couch'} + results = furniture.find(query) + for entry in results: + pprint.pprint(entry) + + log.info('Step 4: Delete the collection so we can start over') + db.drop_collection('furniture') diff --git a/Student/kristianjf/lesson08/nosql_activity/src/neo4j_script.py b/Student/kristianjf/lesson08/nosql_activity/src/neo4j_script.py new file mode 100644 index 0000000..958a8bd --- /dev/null +++ b/Student/kristianjf/lesson08/nosql_activity/src/neo4j_script.py @@ -0,0 +1,210 @@ +""" + neo4j example +""" + + +import utilities +import login_database +import utilities + +log = utilities.configure_logger('default', '../logs/neo4j_script.log') + + +def run_example(): + + log.info('Step 1: First, clear the entire database, so we can start over') + log.info("Running clear_all") + + driver = login_database.login_neo4j_cloud() + with driver.session() as session: + session.run("MATCH (n) DETACH DELETE n") + + log.info("Step 2: Add a few people") + + with driver.session() as session: + + log.info('Adding a few Person nodes') + log.info('The cyph language is analagous to sql for neo4j') + for first, last in [('Bob', 'Jones'), + ('Nancy', 'Cooper'), + ('Alice', 'Cooper'), + ('Fred', 'Barnes'), + ('Mary', 'Evans'), + ('Marie', 'Curie'), + ]: + cyph = "CREATE (n:Person {first_name:'%s', last_name: '%s'})" % ( + first, last) + session.run(cyph) + + log.info("Step 3: Get all of people in the DB:") + cyph = """MATCH (p:Person) + RETURN p.first_name as first_name, p.last_name as last_name + """ + result = session.run(cyph) + print("People in database:") + for record in result: + print(record['first_name'], record['last_name']) + + log.info('Step 4: Create some relationships') + log.info("Bob Jones likes Alice Cooper, Fred Barnes and Marie Curie") + + for first, last in [("Alice", "Cooper"), + ("Fred", "Barnes"), + ("Marie", "Curie")]: + cypher = """ + MATCH (p1:Person {first_name:'Bob', last_name:'Jones'}) + CREATE (p1)-[friend:FRIEND]->(p2:Person {first_name:'%s', last_name:'%s'}) + RETURN p1 + """ % (first, last) + session.run(cypher) + + log.info("Step 5: Find all of Bob's friends") + cyph = """ + MATCH (bob {first_name:'Bob', last_name:'Jones'}) + -[:FRIEND]->(bobFriends) + RETURN bobFriends + """ + result = session.run(cyph) + print("Bob's friends are:") + for rec in result: + for friend in rec.values(): + print(friend['first_name'], friend['last_name']) + + log.info("Setting up Marie's friends") + + for first, last in [("Mary", "Evans"), + ("Alice", "Cooper"), + ('Fred', 'Barnes'), + ]: + cypher = """ + MATCH (p1:Person {first_name:'Marie', last_name:'Curie'}) + CREATE (p1)-[friend:FRIEND]->(p2:Person {first_name:'%s', last_name:'%s'}) + RETURN p1 + """ % (first, last) + + session.run(cypher) + + print("Step 6: Find all of Marie's friends?") + cyph = """ + MATCH (marie {first_name:'Marie', last_name:'Curie'}) + -[:FRIEND]->(friends) + RETURN friends + """ + result = session.run(cyph) + print("\nMarie's friends are:") + for rec in result: + for friend in rec.values(): + print(friend['first_name'], friend['last_name']) + +def run_example_activity(): + + log.info('Step 1: First, clear the entire database, so we can start over') + log.info("Running clear_all") + + driver = login_database.login_neo4j_cloud() + with driver.session() as session: + session.run("MATCH (n) DETACH DELETE n") + + log.info("Step 2: Add a few people") + + with driver.session() as session: + + log.info('Adding a few Person nodes') + log.info('The cyph language is analagous to sql for neo4j') + for first, last in [('Bob', 'Jones'), + ('Nancy', 'Cooper'), + ('Alice', 'Cooper'), + ('Fred', 'Barnes'), + ('Mary', 'Evans'), + ('Marie', 'Curie'), + ]: + cyph = "CREATE (n:Person {first_name:'%s', last_name: '%s'})" % ( + first, last) + session.run(cyph) + + log.info('Adding a few Color nodes') + for color in [('red'), + ('blue'), + ('green') + ]: + cyph = "CREATE (n:Color {color:'%s'})" % ( + color) + session.run(cyph) + + log.info("Step 3: Get all of people in the DB:") + cyph = """MATCH (p:Person) + RETURN p.first_name as first_name, p.last_name as last_name + """ + result = session.run(cyph) + print("People in database:") + for record in result: + print(record['first_name'], record['last_name']) + + log.info('Step 4: Create some relationships') + log.info("Bob Jones likes Alice Cooper, Fred Barnes and Marie Curie") + + for first, last in [("Alice", "Cooper"), + ("Fred", "Barnes"), + ("Marie", "Curie")]: + cypher = """ + MATCH (n1:Color {color:'red'}) + CREATE (n1)-[favorite_color:FAV_COLOR]->(n2:Person {first_name:'%s', last_name:'%s'}) + RETURN n1 + """ % (first, last) + session.run(cypher) + + for first, last in [('Bob', 'Jones'), + ('Nancy', 'Cooper'), + ('Mary', 'Evans')]: + cypher = """ + MATCH (n1:Color {color:'blue'}) + CREATE (n1)-[favorite_color:FAV_COLOR]->(n2:Person {first_name:'%s', last_name:'%s'}) + RETURN n1 + """ % (first, last) + session.run(cypher) + + for first, last in [('Bob', 'Jones'), + ('Fred', 'Barnes'), + ('Mary', 'Evans')]: + cypher = """ + MATCH (n1:Color {color:'green'}) + CREATE (n1)-[favorite_color:FAV_COLOR]->(n2:Person {first_name:'%s', last_name:'%s'}) + RETURN n1 + """ % (first, last) + session.run(cypher) + + log.info("Step 5: Find all person's who favor red color") + cyph = """ + MATCH (color {color:'red'}) + -[:FAV_COLOR]->(personColors) + RETURN personColors + """ + result = session.run(cyph) + print("Red's favorited by:") + for rec in result: + for person in rec.values(): + print(person['first_name'], person['last_name']) + + log.info("Step 5: Find all person's who favor blue color") + cyph = """ + MATCH (color {color:'blue'}) + -[:FAV_COLOR]->(personColors) + RETURN personColors + """ + result = session.run(cyph) + print("Blue's favorited by:") + for rec in result: + for person in rec.values(): + print(person['first_name'], person['last_name']) + + log.info("Step 5: Find all person's who favor green color") + cyph = """ + MATCH (color {color:'green'}) + -[:FAV_COLOR]->(personColors) + RETURN personColors + """ + result = session.run(cyph) + print("Green's favorited by:") + for rec in result: + for person in rec.values(): + print(person['first_name'], person['last_name']) diff --git a/Student/kristianjf/lesson08/nosql_activity/src/redis_script.py b/Student/kristianjf/lesson08/nosql_activity/src/redis_script.py new file mode 100644 index 0000000..16cb5ad --- /dev/null +++ b/Student/kristianjf/lesson08/nosql_activity/src/redis_script.py @@ -0,0 +1,84 @@ +""" + demonstrate use of Redis +""" + + +import login_database +import utilities + + +def run_example(): + """ + uses non-presistent Redis only (as a cache) + + """ + + log = utilities.configure_logger('default', '../logs/redis_script.log') + + try: + log.info('Step 1: connect to Redis') + r = login_database.login_redis_cloud() + log.info('Step 2: cache some data in Redis') + r.set('andy', 'andy@somewhere.com') + + log.info('Step 2: now I can read it') + email = r.get('andy') + log.info('But I must know the key') + log.info(f'The results of r.get: {email}') + + log.info('Step 3: cache more data in Redis') + r.set('pam', 'pam@anywhere.com') + r.set('fred', 'fred@fearless.com') + + log.info('Step 4: delete from cache') + r.delete('andy') + log.info(f'r.delete means andy is now: {email}') + + log.info( + 'Step 6: Redis can maintain a unique ID or count very efficiently') + r.set('user_count', 21) + r.incr('user_count') + r.incr('user_count') + r.decr('user_count') + result = r.get('user_count') + log.info('I could use this to generate unique ids') + log.info(f'Redis says 21+1+1-1={result}') + + log.info('Step 7: richer data for a SKU') + r.rpush('186675', 'chair') + r.rpush('186675', 'red') + r.rpush('186675', 'leather') + r.rpush('186675', '5.99') + + log.info('Step 8: pull some data from the structure') + cover_type = r.lindex('186675', 2) + log.info(f'Type of cover = {cover_type}') + + except Exception as e: + print(f'Redis error: {e}') + +def run_example_activity(): + """ + uses non-presistent Redis only (as a cache) + + """ + + log = utilities.configure_logger('default', '../logs/redis_script.log') + + try: + log.info('Step 1: connect to Redis') + r = login_database.login_redis_cloud() + log.info('Step 2: Have Redis store a customer name, telephone and zip for 6 or so customers') + r.rpush('andy', '111-111-1111', '11111') + r.rpush('bonnie', '222-222-2222', '22222') + r.rpush('camden', '333-333-3333', '33333') + r.rpush('duke', '444-444-4444', '44444') + r.rpush('ethridge', '555-555-5555', '55555') + r.rpush('foster', '777-777-7777', '77777') + + log.info('Step 3: Then show how you can retrieve a zip code, and then a phone number, for a known customer') + log.info(f'Andy Phone: {r.lindex("andy", 0)}') + log.info(f'Camden Zip: {r.lindex("camden", 1)}') + + except Exception as e: + print(f'Redis error: {e}') diff --git a/Student/kristianjf/lesson08/nosql_activity/src/simple_script_activity.py b/Student/kristianjf/lesson08/nosql_activity/src/simple_script_activity.py new file mode 100644 index 0000000..a9e9663 --- /dev/null +++ b/Student/kristianjf/lesson08/nosql_activity/src/simple_script_activity.py @@ -0,0 +1,98 @@ +""" +pickle etc +""" + +import pickle + +import pprint +import utilities + +log = utilities.configure_logger('default', '../logs/mongodb_script.log') + +def run_pickle_activity(): + """ + Write and read with pickle + """ + log.info("\n\n====") + log.info('Step 1: Demonstrate persistence with pickle') + log.info('Create some data (at least 10 rows with about 5 fields in each).') + lakers_lineup = [ + { + 'name': 'JaVale McGee', + 'age': 30, + 'position': 'center', + 'ppg': 4.8, + 'salary': 2393887 + }, + { + 'product': 'Kyle Kuzma', + 'age': 22, + 'position': 'power forward', + 'ppg': 16.1, + 'salary': 1689840 + }, + { + 'product': 'Brandon Ingram', + 'age': 20, + 'position': 'small forward', + 'ppg': 16.1, + 'salary': 5757120 + }, + { + 'product': 'LeBron James', + 'age': 33, + 'position': 'shooting guard', + 'ppg': 27.5, + 'salary': 35600000 + }, + { + 'product': 'Rajon Rondo', + 'age': 32, + 'position': 'point guard', + 'ppg': 8.3, + 'salary': 9000000 + }, + { + 'name': 'Mo Wagner', + 'age': 21, + 'position': 'center', + 'ppg': 'N/A', + 'salary': 1764240 + }, + { + 'product': 'Isaac Bonga', + 'age': 18, + 'position': 'power forward', + 'ppg': 'N/A', + 'salary': 'N/A' + }, + { + 'product': 'Luol Deng', + 'age': 33, + 'position': 'small forward', + 'ppg': 7.6, + 'salary': 18000000 + }, + { + 'product': 'Kentavious Caldwell-Pope', + 'age': 25, + 'position': 'shooting guard', + 'ppg': 13.4, + 'salary': 18000000 + }, + { + 'product': 'Lonzo Ball', + 'age': 20, + 'position': 'point guard', + 'ppg': 10.2, + 'salary': 7461960 + } + ] + pickle.dump(lakers_lineup, open('../data/data.pkl', 'wb')) + + log.info('Step 2: Now read it back from the pickle file') + read_data = pickle.load(open('../data/data.pkl', 'rb')) + log.info('Step 3: Show that the write and read were successful') + assert read_data == lakers_lineup + log.info("and print the data") + pprint.pprint(read_data) diff --git a/Student/kristianjf/lesson08/nosql_activity/src/utilities.py b/Student/kristianjf/lesson08/nosql_activity/src/utilities.py new file mode 100644 index 0000000..15b1079 --- /dev/null +++ b/Student/kristianjf/lesson08/nosql_activity/src/utilities.py @@ -0,0 +1,43 @@ +""" +enable easy and controllable logging +""" + +import logging +import logging.config + + +def configure_logger(name, log_path): + """ + generic logger + """ + logging.config.dictConfig({ + 'version': 1, + 'formatters': { + 'default': {'format': '%(asctime)s - %(levelname)s - %(message)s', 'datefmt': '%Y-%m-%d %H:%M:%S'} + }, + 'handlers': { + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'default', + 'stream': 'ext://sys.stdout' + }, + 'file': { + 'level': 'DEBUG', + 'class': 'logging.handlers.RotatingFileHandler', + 'formatter': 'default', + 'filename': log_path, + 'maxBytes': 1024, + 'backupCount': 3 + } + }, + 'loggers': { + 'default': { + 'level': 'DEBUG', + 'handlers': ['console', 'file'] + } + }, + 'disable_existing_loggers': False + }) + return logging.getLogger(name) + diff --git a/Student/kristianjf/lesson08/nosql_assignment/logs/login_databases_dev.log b/Student/kristianjf/lesson08/nosql_assignment/logs/login_databases_dev.log new file mode 100644 index 0000000..88033e7 --- /dev/null +++ b/Student/kristianjf/lesson08/nosql_assignment/logs/login_databases_dev.log @@ -0,0 +1,2 @@ +2018-07-04 20:17:27 - INFO - Here is where we use the connect to neo4j. +2018-07-04 20:17:27 - INFO - diff --git a/Student/kristianjf/lesson08/nosql_assignment/src/login_database.py b/Student/kristianjf/lesson08/nosql_assignment/src/login_database.py new file mode 100644 index 0000000..0a06b14 --- /dev/null +++ b/Student/kristianjf/lesson08/nosql_assignment/src/login_database.py @@ -0,0 +1,86 @@ +""" + module that will login to the various demonstration databases consistently +""" + +import configparser +from pathlib import Path +import pymongo +import redis +from neo4j.v1 import GraphDatabase, basic_auth + +import utilities + +log = utilities.configure_logger('default', '../logs/login_databases_dev.log') +config_file = Path(__file__).parent.parent / '.config/config.ini' +config = configparser.ConfigParser() + + +def login_mongodb_cloud(): + """ + connect to mongodb and login + """ + + log.info('Here is where we use the connect to mongodb.') + log.info('Note use of f string to embed the user & password (from the tuple).') + try: + config.read(config_file) + user = config["mongodb_cloud"]["user"] + pw = config["mongodb_cloud"]["pw"] + connection = config["mongodb_cloud"]["connection"] + + except Exception as e: + print(f'error: {e}') + + # client = pymongo.MongoClient(f'mongodb://{user}:{pw}' + # '@cluster0-shard-00-00-wphqo.mongodb.net:27017,' + # 'cluster0-shard-00-01-wphqo.mongodb.net:27017,' + # 'cluster0-shard-00-02-wphqo.mongodb.net:27017/test' + # '?ssl=true&replicaSet=Cluster0-shard-0&authSource=admin') + client = pymongo.MongoClient(connection.format(user=user, pw=pw)) + + return client + + +def login_redis_cloud(): + """ + connect to redis and login + """ + try: + config.read(config_file) + host = config["redis_cloud"]["host"] + port = config["redis_cloud"]["port"] + pw = config["redis_cloud"]["pw"] + + + except Exception as e: + print(f'error: {e}') + + log.info('Here is where we use the connect to redis.') + + try: + r = redis.StrictRedis(host=host, port=port, password=pw, decode_responses=True) + + except Exception as e: + print(f'error: {e}') + + return r + + +def login_neo4j_cloud(): + """ + connect to neo4j and login + + """ + + log.info('Here is where we use the connect to neo4j.') + log.info('') + + config.read(config_file) + + graphenedb_user = config["neo4j_cloud"]["user"] + graphenedb_pass = config["neo4j_cloud"]["pw"] + graphenedb_url = 'bolt://hobby-opmhmhgpkdehgbkejbochpal.dbs.graphenedb.com:24786' + driver = GraphDatabase.driver(graphenedb_url, + auth=basic_auth(graphenedb_user, graphenedb_pass)) + + return driver diff --git a/Student/kristianjf/lesson08/nosql_assignment/src/mailroom_6.py b/Student/kristianjf/lesson08/nosql_assignment/src/mailroom_6.py new file mode 100644 index 0000000..ea99d85 --- /dev/null +++ b/Student/kristianjf/lesson08/nosql_assignment/src/mailroom_6.py @@ -0,0 +1,338 @@ +#!/usr/bin/env python3 +import sys +import logging +import login_database +from datetime import datetime +from functools import reduce +''' +Goal: +You work in the mail room at a local charity. Part of your job is to write \ +incredibly boring, repetitive emails thanking your donors for their generous \ +gifts. You are tired of doing this over and over again, so you’ve decided to \ +let Python help you out of a jam and do your work for you. +''' +''' +It should have a data structure that holds a list of your donors and a history of \ +the amounts they have donated. This structure should be populated at first with at \ +least five donors, with between 1 and 3 donations each. +''' + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +logger.info('Connecting to MongoDB') +database = login_database.login_mongodb_cloud()['dev_assignment'] +logger.info(f'''Connected to {database.client} and + created database {database.__dict__.get('_Database__name')}''') + +class Donor(): + database = login_database.login_mongodb_cloud()['dev_assignment'] + donor_collection = database['donors'] + r_cache = login_database.login_redis_cloud() + neo4j_driver = login_database.login_neo4j_cloud() + def __init__(self, name, donations): + self._name = name.title() + self._donations = donations if isinstance(donations, list) else [donations] + '''Add Donor Record to Table''' + try: + self.donor_collection.insert_one({'name': self._name, 'donations':self._donations}) + with self.neo4j_driver.session() as session: + cyph = "CREATE (n:Donor {donor:'%s'})" % ( + self._name) + session.run(cyph) + cyph = "CREATE (n:Donation {donation:'%s'})" % ( + self._donations) + session.run(cyph) + cypher = """ + MATCH (n1:Donor {donor:'%s'}) + CREATE (n1)-[donations:DONATIONS]->(n2:Donation {donations:'%s'}) + """ % (self._name, self._donations) + session.run(cypher) + + except Exception as e: + logger.info(f'Error creating = {name, donations}') + logger.info(e) + else: + donations = self.donations + for donation in donations: + self.r_cache.rpush(self._name, donation) + finally: + logger.info('database closes') + database.client.close() + + def __lt__(self, other): + return sum(self.donations) < sum(other.donations) + @property + def name(self): + return self._name + @property + def donations(self): + if self.r_cache.lindex(self.name, 0): + return [int(donation) for donation in self.r_cache.lrange(self.name, 0, -1)] + else: + for donations in self.donor_collection.find({'name': self.name}): + return donations['donations'] + @property + def normalized_name(self): + return self._name + def donate(self, new_donation): + try: + donations = self.donations + donations = self.donations.append(new_donation) + self.donor_collection.update_one({'name': self.name}, + {'$set': {'donations': donations}}) + except Exception as e: + logger.info(f'Error adding donation = {self.name, new_donation}') + logger.info(e) + else: + self.r_cache.rpush(self._name, new_donation) + + def metrics(self): + num_d = len(self.donations) + total_d = sum(self.donations) + avg_d = total_d / num_d + return num_d, total_d, avg_d + + def send_letter(self): + ''' + Try to use a dict and the .format() method to do the letter as one big \ + template rather than building up a big string in parts. + In this version, add a function (and a menu item to invoke it), that goes \ + through all the donors in your donor data structure, generates a thank you \ + letter, and writes it to disk as a text file. + ''' + filename = '_'.join(self.name.split())+'.txt' + with open(filename, 'w') as outfile: + outfile.write(self.output_string().format(name=self.name, donation=self.donations)) + print(f'Writing: {filename}') + + def output_string(self): + 'Output string for letters' + format_string = 'Dear {name},\n\nThank you for your generous donation of ' \ + '${donation}. Please send us more money at your earliest ' \ + 'convenience.' + return format_string + +class DonorHandler: + def __init__(self): + self.donors = {} + def add_donor(self, donor_object): + self.donors[donor_object.normalized_name] = donor_object + def dnr_challenge(self, factor=1, **kwargs): + new_dh = DonorHandler() + for donor, donor_object in self.donors.items(): + '''Add a new feature to Mailroom using filter so that donations either above + or below a specified dollar amount are included in the map operations of #1 above. + You can do this by adding min_donation and max_donation optional keyword parameters + to your challenge function. You’ll want to filter the donations before + passing them to map.''' + filter_donations = self.dnr_filter(\ + donor_object.donations, kwargs['min_donation'], kwargs['max_donation']) + + '''Add a new feature to Mailroom using map so that each donation on record can be + doubled, tripled or indeed multiplied by any arbitrary factor based on the whims + of philanthropists who would like to support our cause. + This will require a new function (or method in your donor database class) called + challenge(factor) that takes a multiplier (factor), and multiplies all the + donations of all the donors by the factor. The function returns a NEW donor database, + with the new data.''' + map_donations_f = list(map(lambda x, y: x*y, filter_donations,\ + len(filter_donations)*[factor])) + + new_d = Donor(donor+' Projection', map_donations_f) + new_dh.add_donor(new_d) + return new_dh + def dnr_filter(self, lst, min_donation=0, max_donation=10000000): + lst = list(filter(lambda x: (x > min_donation) and (x < max_donation), lst)) + return lst + + def dnr_projections(self, donor, **kwargs): + ''' + For instance, based on donations in the current database, show them (a) what their total + contribution would come to in dollars if they were to double contributions under $100. + And then (b) show them what their total contribution would come to if they were to + triple contributions over $50. + ''' + dnr_proj = self.dnr_challenge(**kwargs) + return reduce(lambda x, y: x+y, dnr_proj.donors[donor].donations) + + def thank_you(self, full_name='', donation_amount=int(), **kwargs): + ''' + If the user (you) selects ‘Send a Thank You’, prompt for a Full Name. + If the user types a name not in the list, add that name to the data structure and use it. + If the user types a name in the list, use it. + Once a name has been selected, prompt for a donation amount. + Turn the amount into a number – it is OK at this point for the program to crash if \ + someone types a bogus amount. + Once an amount has been given, add that amount to the donation history of the selected user. + Finally, use string formatting to compose an email thanking the donor for their generous \ + donation. Print the email to the terminal and return to the original prompt. + ''' + ''' + Collect, Format and Validate Full name + ''' + while not full_name: + full_name_input = input('Please enter full name of the recipient: ').split() + full_name = self.name_response_valid(full_name_input) + ''' + Process Full Name + ''' + ds_names = [name for name in self.donors] + + if full_name in ds_names: + print(f'Found {full_name} in data_structure') + while not donation_amount: + donation_amount = int(input('Please enter a donation amount: ')) + self.donors[full_name].donate(donation_amount) + print(self.donors[full_name].output_string().format(name=full_name, \ + donation=donation_amount)) + else: + while not donation_amount: + donation_amount = int(input('Please enter a donation amount: ')) + self.add_donor(Donor(full_name, donation_amount)) + print(self.donors[full_name].output_string().format(name=full_name, \ + donation=donation_amount)) + return self.donors[full_name].output_string().format(name=full_name, \ + donation=donation_amount) + + def list_donors(self, **kwargs): + ''' + If the user types ‘list’, show them a list of the donor names and re-prompt + ''' + for name in self.donors: + print(name) + return [name for name in self.donors] + + def create_a_report(self, **kwargs): + ''' + Creating a Report + If the user (you) selected “Create a Report”, print a list of your donors, \ + sorted by total historical donation amount. + ''' + print('Menu: Create a Report') + top_row = ('Donor Name', 'Total Given', 'Num Gifts', 'Average Gift') + format_header = '{:<30} | {:<15} | {:<10} | {:<15}\n' + format_data = '{:<30} $ {:<15.2f} {:<10} $ {:<15.2f}\n' + format_string = format_header.format(*top_row) + for donor in sorted(self.donors.values(), reverse=True): + name = donor.name + num_gifts, total_given, avg_gift = donor.metrics() + format_string += format_data.format(name, total_given, num_gifts, avg_gift) + print(format_string) + return format_string + + def send_letters(self, **kwargs): + ''' + Try to use a dict and the .format() method to do the letter as one big \ + template rather than building up a big string in parts. + In this version, add a function (and a menu item to invoke it), that goes \ + through all the donors in your donor data structure, generates a thank you \ + letter, and writes it to disk as a text file. + ''' + for donor in self.donors.values(): + donor.send_letter() + + def run_projection(self, full_name='', max_donation_amount=int(), factor=int(), **kwargs): + 'Select from list of Donors to Run Projections' + # donor_list_dict = {full_name: self.dnr_projections for donor in self.donors} + donor_list_dict = {donor: kwargs.update({'full_name': donor}) for donor in self.donors} + menu(donor_list_dict) + 'Collect Donor Name, Max Donation Amount, Factor' + while not max_donation_amount: + max_donation_amount = int(input('Please enter a max donation amount: ')) + while not factor: + factor = int(input('Please enter a factor: ')) + donation_amount = self.dnr_projections(donor=full_name, min_donation=1, \ + max_donation=max_donation_amount, factor=factor, **kwargs) + print(self.donors[full_name].output_string().format(name=full_name, \ + donation=donation_amount)) + return self.donors[full_name].output_string().format(name=full_name, \ + donation=donation_amount) + + #Name Validation + def name_response_valid(self, full_name): + 'Validate and Return Name Input' + full_name_cap = '' + try: + for name in full_name: + assert name.isalpha() + full_name_cap += f'{name.capitalize()} ' + full_name = full_name_cap.strip() + except AssertionError: + print('Invalid Name. Found non-alphabetic characters') + else: + return full_name + +#Function to Process Menu Options +def menu(options_dict, **kwargs): + ''' + The script should prompt the user (you) to choose from a menu of 3 actions: \ + “Send a Thank You”, “Create a Report” or “quit”) + ''' + options = [option[0:2] for option in enumerate(options_dict.keys(), 1)] + while True: + print('Please select a number from the list of the following options: \n') + for option in options: + print(option) + response = menu_response_valid(options) + response_selection = options[response-1][1] + options_dict[response_selection](**kwargs) + +#Main Menu +def main_menu(**kwargs): + 'Create Main Menu' + program_options_dict = {'Send a Thank You': thank_you_menu, \ + 'Create a Report': kwargs['donor_handler'].create_a_report, \ + 'Send Letters to Everyone': kwargs['donor_handler'].send_letters, \ + 'Run Donor Projections': kwargs['donor_handler'].run_projection, \ + 'quit': quit_menu} + menu(program_options_dict, **kwargs) + +#Thank You Menu +def thank_you_menu(**kwargs): + ''' + Sending a Thank You + If the user (you) selects ‘Send a Thank You’, prompt for a Full Name. + If the user types ‘list’, show them a list of the donor names and re-prompt + ''' + program_options_ty = {'Send a Thank You': kwargs['donor_handler'].thank_you, \ + 'Action: List': kwargs['donor_handler'].list_donors, \ + 'quit': main_menu} + menu(program_options_ty, **kwargs) + +def quit_menu(**kwargs): + sys.exit() + +#Menu Option Validation +def menu_response_valid(options): + 'Validate Menu Options' + try: + response = int(input()) + assert response in [option[0] for option in options] + except ValueError: + print('Non-integer value entered. Please try again.') + except AssertionError: + print('This number does not exist in the list of options. Please try again.') + else: + return response + #Name Validation + +def PopulateDB(): + 'Create Donors' + jb = Donor('Jeff Bezos', [1, 5, 10]) + bg = Donor('Bill Gates', [10000]) + sj = Donor('Steve Jobs', [20, 50, 100]) + 'Create Donors Handle' + dh = DonorHandler() + dh.add_donor(jb) + dh.add_donor(bg) + dh.add_donor(sj) + return dh + +def main(): + dh = PopulateDB() + 'Call Main Menu' + main_menu(donor_handler=dh) + +if __name__ == '__main__': + main() diff --git a/Student/kristianjf/lesson08/nosql_assignment/src/test_mailroom_6.py b/Student/kristianjf/lesson08/nosql_assignment/src/test_mailroom_6.py new file mode 100644 index 0000000..9cfdb97 --- /dev/null +++ b/Student/kristianjf/lesson08/nosql_assignment/src/test_mailroom_6.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 + +import mailroom_6 as m6 + +def clear_donations(donor_name): + m6.Donor.donor_collection.delete_many({"name": donor_name}) + m6.Donor.r_cache.delete(donor_name) + +def test_donor(): + clear_donations('Kristian Francisco') + kf = m6.Donor('Kristian Francisco', [1000, 100]) + test_metrics = kf.metrics() + assert kf.name == 'Kristian Francisco' + assert test_metrics[0] == 2 + assert test_metrics[1] == 1100 + assert test_metrics[2] == 550.0 + clear_donations('Kristian Francisco') + +def test_donor_handle(): + clear_donations('Kristian Francisco') + kf = m6.Donor('Kristian Francisco', [1000, 100]) + dh = m6.DonorHandler() + dh.add_donor(kf) + assert 'Kristian Francisco' in dh.donors + assert dh.donors['Kristian Francisco'].donations == [1000, 100] + clear_donations('Kristian Francisco') + +def test_donor_challenge(): + clear_donations('Kristian Francisco') + clear_donations('Kristian Francisco Projection') + kf = m6.Donor('Kristian Francisco', [5, 1000, 100]) + dh = m6.DonorHandler() + dh.add_donor(kf) + dh2 = dh.dnr_challenge(factor=4, min_donation=10, max_donation=1000) + print(dh2.donors['Kristian Francisco Projection'].donations) + assert dh2.donors['Kristian Francisco Projection'].donations == [400] + # assert False + clear_donations('Kristian Francisco') + clear_donations('Kristian Francisco Projection') + +def test_donor_projection(): + clear_donations('Kristian Francisco') + clear_donations('Kristian Francisco Projection') + kf = m6.Donor('Kristian Francisco', [20, 1000, 100]) + dh = m6.DonorHandler() + dh.add_donor(kf) + prj_1 = dh.dnr_projections('Kristian Francisco Projection', factor=4, min_donation=10, max_donation=1000) + assert prj_1 == 480 + clear_donations('Kristian Francisco') + clear_donations('Kristian Francisco Projection') diff --git a/Student/kristianjf/lesson08/nosql_assignment/src/utilities.py b/Student/kristianjf/lesson08/nosql_assignment/src/utilities.py new file mode 100644 index 0000000..15b1079 --- /dev/null +++ b/Student/kristianjf/lesson08/nosql_assignment/src/utilities.py @@ -0,0 +1,43 @@ +""" +enable easy and controllable logging +""" + +import logging +import logging.config + + +def configure_logger(name, log_path): + """ + generic logger + """ + logging.config.dictConfig({ + 'version': 1, + 'formatters': { + 'default': {'format': '%(asctime)s - %(levelname)s - %(message)s', 'datefmt': '%Y-%m-%d %H:%M:%S'} + }, + 'handlers': { + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'default', + 'stream': 'ext://sys.stdout' + }, + 'file': { + 'level': 'DEBUG', + 'class': 'logging.handlers.RotatingFileHandler', + 'formatter': 'default', + 'filename': log_path, + 'maxBytes': 1024, + 'backupCount': 3 + } + }, + 'loggers': { + 'default': { + 'level': 'DEBUG', + 'handlers': ['console', 'file'] + } + }, + 'disable_existing_loggers': False + }) + return logging.getLogger(name) +