diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..9d866e3 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "pip" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cd896e0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +.venv +__pycache__ +*.tar.gz +*.zip +extract_tar +extract_zip +raef.txt +example.bin +fear_copy.txt +write_x.txt +data.pickle +*.db +.pytest_cache +df.csv +df.json +df.xlsx \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..fa79a94 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Current File", + "type": "python", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "justMyCode": true, + "cwd": "${fileDirname}" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6ea95d5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.testing.pytestArgs": [ + "ch10" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/ch01/README.md b/ch01/README.md index 56d08a5..4e6d6f4 100644 --- a/ch01/README.md +++ b/ch01/README.md @@ -1,5 +1,4 @@ -Chapter 1 data files -==================== +# Chapter 1 data files The files in this folder are not supposed to work if run. They serve as source for the book chapters, and to provide a diff --git a/ch01/bike.py b/ch01/bike.py index 223c483..8cc51d7 100644 --- a/ch01/bike.py +++ b/ch01/bike.py @@ -1,6 +1,5 @@ # let's define the class Bike class Bike: - def __init__(self, colour, frame_material): self.colour = colour self.frame_material = frame_material @@ -8,9 +7,10 @@ def __init__(self, colour, frame_material): def brake(self): print("Braking!") + # let's create a couple of instances -red_bike = Bike('Red', 'Carbon fiber') -blue_bike = Bike('Blue', 'Steel') +red_bike = Bike("Red", "Carbon fiber") +blue_bike = Bike("Blue", "Steel") # let's inspect the objects we have, instances of the Bike class. print(red_bike.colour) # prints: Red diff --git a/ch01/factorial.py b/ch01/factorial.py index 2944556..22c100e 100644 --- a/ch01/factorial.py +++ b/ch01/factorial.py @@ -1,3 +1,3 @@ ->>> from math import factorial ->>> factorial(5) -120 \ No newline at end of file +from math import factorial + +print(factorial(5)) diff --git a/ch01/names.py b/ch01/names.py index 2faca7f..5ec1d48 100644 --- a/ch01/names.py +++ b/ch01/names.py @@ -1,19 +1,21 @@ ->>> n = 3 # integer number ->>> address = "221b Baker Street, NW1 6XE, London" # Sherlock Holmes' address ->>> employee = { -... 'age': 45, -... 'role': 'CTO', -... 'SSN': 'AB1234567', -... } ->>> # let's print them ->>> n -3 ->>> address -'221b Baker Street, NW1 6XE, London' ->>> employee -{'age': 45, 'role': 'CTO', 'SSN': 'AB1234567'} ->>> other_name -Traceback (most recent call last): - File "", line 1, in -NameError: name 'other_name' is not defined ->>> +import pprint + +pp = pprint.PrettyPrinter(indent=4) + +n = 3 # integer number +address = "221b Baker Street, NW1 6XE, London" # Sherlock Holmes' address +employee = { + "age": 45, + "role": "CTO", + "SSN": "AB1234567", +} + +# let's print them +print(n) +print(address) +pp.pprint(employee) + +# other_name +# Traceback (most recent call last): +# File "", line 1, in +# NameError: name 'other_name' is not defined diff --git a/ch01/scopes1.py b/ch01/scopes1.py index 4d82c3b..0960153 100644 --- a/ch01/scopes1.py +++ b/ch01/scopes1.py @@ -1,10 +1,12 @@ # Local versus Global + # we define a function, called local def local(): m = 7 print(m) + # we define m within the global scope m = 5 diff --git a/ch01/scopes2.py b/ch01/scopes2.py index 70b115b..ba39aca 100644 --- a/ch01/scopes2.py +++ b/ch01/scopes2.py @@ -1,12 +1,14 @@ # Local versus Global + def local(): # m doesn't belong to the scope defined by the local function # so Python will keep looking into the next enclosing scope. # m is finally found in the global scope - print(m, 'printing from the local scope') + print(m, "printing from the local scope") + m = 5 -print(m, 'printing from the global scope') +print(m, "printing from the global scope") local() diff --git a/ch01/scopes3.py b/ch01/scopes3.py index 535a80b..24bb823 100644 --- a/ch01/scopes3.py +++ b/ch01/scopes3.py @@ -9,12 +9,13 @@ def local(): # function so Python will keep looking into the next # enclosing scope. This time m is found in the enclosing # scope - print(m, 'printing from the local scope') + print(m, "printing from the local scope") # calling the function local local() + m = 5 -print(m, 'printing from the global scope') +print(m, "printing from the global scope") enclosing_func() diff --git a/ch01/virtualenv.creation.txt b/ch01/virtualenv.creation.txt deleted file mode 100644 index 1ffaf27..0000000 --- a/ch01/virtualenv.creation.txt +++ /dev/null @@ -1,65 +0,0 @@ -Ubuntu 20.04 - -https://www.liquidweb.com/kb/how-to-install-and-update-python-to-3-9-in-ubuntu - -# optional installation steps -fab@fvm:~$ sudo apt-get update -fab@fvm:~$ sudo apt-get install software-properties-common -fab@fvm:~$ sudo add-apt-repository ppa:deadsnakes/ppa -fab@fvm:~$ sudo apt-get update -fab@fvm:~$ sudo apt-get install python3.9 python3.9-venv python3.9-dev - -fab@fvm:~/srv$ mkdir my-project # step 1 -fab@fvm:~/srv$ cd my-project - -fab@fvm:~/srv/my-project$ which python3.9 # check system python -/usr/bin/python3.9 # <-- system python3.9 - -fab@fvm:~/srv/my-project$ python3.9 -m venv lpp3ed # step 2 -fab@fvm:~/srv/my-project$ source ./lpp3ed/bin/activate # step 3 - -# check python again: now using the virtual environment's one -(lpp3ed) fab@fvm:~/srv/my-project$ which python -/home/fab/srv/my-project/lpp3ed/bin/python - -(lpp3ed) fab@fvm:~/srv/my-project$ python # step 4 -Python 3.9.2 (default, Feb 20 2021, 20:56:08) -[GCC 9.3.0] on linux -Type "help", "copyright", "credits" or "license" for more information. ->>> exit() - -(lpp3ed) fab@fvm:~/srv/my-project$ deactivate # step 5 -fab@fvm:~/srv/my-project$ - - ----------------------------------------------------------------------------------------- - -Windows 10 -Simply install from python website, then from terminal: - -C:\Users\Fab\srv>mkdir my-project # step 1 - -C:\Users\Fab\srv>cd my-project - -C:\Users\Fab\srv\my-project>where python # check system python -C:\Users\Fab\AppData\Local\Programs\Python\Python39\python.exe -C:\Users\Fab\AppData\Local\Microsoft\WindowsApps\python.exe - -C:\Users\Fab\srv\my-project>python -m venv lpp3ed # step 2 - -C:\Users\Fab\srv\my-project>lpp3ed\Scripts\activate # step 3 - -# check python again, now virtual env python is listed first -(lpp3ed) C:\Users\Fab\srv\my-project>where python -C:\Users\Fab\srv\my-project\lpp3ed\Scripts\python.exe -C:\Users\Fab\AppData\Local\Programs\Python\Python39\python.exe -C:\Users\Fab\AppData\Local\Microsoft\WindowsApps\python.exe - -(lpp3ed) C:\Users\Fab\srv\my-project>python # step 4 -Python 3.9.2 (tags/v3.9.2:1a79785, Feb 19 2021, 13:44:55) -⇢ [MSC v.1928 64 bit (AMD64)] on win32 -Type "help", "copyright", "credits" or "license" for more information. ->>> exit() - -(lpp3ed) C:\Users\Fab\srv\my-project>deactivate # step 5 -C:\Users\Fab\srv\my-project> diff --git a/ch02/README.md b/ch02/README.md index a4f9df1..1c72969 100644 --- a/ch02/README.md +++ b/ch02/README.md @@ -1,5 +1,4 @@ -Chapter 2 data files -==================== +# Chapter 2 data files The files in this folder are not supposed to work if run. They serve as source for the book chapters, and to provide a diff --git a/ch02/bytearray.py b/ch02/bytearray.py index a33b972..a4f103a 100644 --- a/ch02/bytearray.py +++ b/ch02/bytearray.py @@ -1,18 +1,11 @@ # bytearray.py +print(bytearray()) # empty bytearray object +print(bytearray(10)) # zero-filled instance with given length +print(bytearray(range(5))) # bytearray from iterable of integers ->>> bytearray() # empty bytearray object -bytearray(b'') ->>> bytearray(10) # zero-filled instance with given length -bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') ->>> bytearray(range(5)) # bytearray from iterable of integers -bytearray(b'\x00\x01\x02\x03\x04') ->>> name = bytearray(b'Lina') #A - bytearray from bytes ->>> name.replace(b'L', b'l') -bytearray(b'lina') ->>> name.endswith(b'na') -True ->>> name.upper() -bytearray(b'LINA') ->>> name.count(b'L') -1 +name = bytearray(b"Lina") # bytearray from bytes +print(name.replace(b"L", b"l")) +print(name.endswith(b"na")) +print(name.upper()) +print(name.count(b"L")) diff --git a/ch02/chainmap.py b/ch02/chainmap.py index 2b637ed..add2404 100644 --- a/ch02/chainmap.py +++ b/ch02/chainmap.py @@ -1,24 +1,20 @@ # chainmap.py +from collections import ChainMap ->>> from collections import ChainMap ->>> default_connection = {'host': 'localhost', 'port': 4567} ->>> connection = {'port': 5678} ->>> conn = ChainMap(connection, default_connection) # map creation ->>> conn['port'] # port is found in the first dictionary -5678 ->>> conn['host'] # host is fetched from the second dictionary -'localhost' ->>> conn.maps # we can see the mapping objects -[{'port': 5678}, {'host': 'localhost', 'port': 4567}] ->>> conn['host'] = 'packtpub.com' # let's add host ->>> conn.maps -[{'port': 5678, 'host': 'packtpub.com'}, - {'host': 'localhost', 'port': 4567}] ->>> del conn['port'] # let's remove the port information ->>> conn.maps -[{'host': 'packtpub.com'}, {'host': 'localhost', 'port': 4567}] ->>> conn['port'] # now port is fetched from the second dictionary -4567 ->>> dict(conn) # easy to merge and convert to regular dictionary -{'host': 'packtpub.com', 'port': 4567} +default_connection = {"host": "localhost", "port": 4567} +connection = {"port": 5678} +conn = ChainMap(connection, default_connection) # map creation +print(conn["port"]) # port is found in the first dictionary + +print(conn["host"]) # host is fetched from the second dictionary +print(conn.maps) # we can see the mapping objects + +conn["host"] = "packtpub.com" # let's add host +print(conn.maps) + +del conn["port"] # let's remove the port information +print(conn.maps) + +print(conn["port"]) # now port is fetched from the second dictionary +print(dict(conn)) # easy to merge and convert to regular dictionary diff --git a/ch02/dateandtime.py b/ch02/dateandtime.py index 3870845..a11bfe9 100644 --- a/ch02/dateandtime.py +++ b/ch02/dateandtime.py @@ -1,143 +1,86 @@ - # imports ->>> from datetime import date, datetime, timedelta, timezone ->>> import time ->>> import calendar as cal ->>> from zoneinfo import ZoneInfo +from datetime import date, datetime, timedelta, timezone +import time +import calendar as cal +from zoneinfo import ZoneInfo +# arrow small demo +import arrow # date ->>> today = date.today() ->>> today -datetime.date(2021, 3, 28) ->>> today.ctime() -'Sun Mar 28 00:00:00 2021' ->>> today.isoformat() -'2021-03-28' ->>> today.weekday() -6 ->>> cal.day_name[today.weekday()] -'Sunday' ->>> today.day, today.month, today.year -(28, 3, 2021) ->>> today.timetuple() -time.struct_time( - tm_year=2021, tm_mon=3, tm_mday=28, - tm_hour=0, tm_min=0, tm_sec=0, - tm_wday=6, tm_yday=87, tm_isdst=-1 -) +today = date.today() +print(today) + +date_a = date(2021, 3, 28) +print(date_a.ctime()) + +print(today.isoformat()) +print(today.weekday()) + +print(cal.day_name[today.weekday()]) +print(today.day, today.month, today.year) +print(today.timetuple()) # time ->>> time.ctime() -'Sun Mar 28 15:23:17 2021' ->>> time.daylight -1 ->>> time.gmtime() -time.struct_time( - tm_year=2021, tm_mon=3, tm_mday=28, - tm_hour=14, tm_min=23, tm_sec=34, - tm_wday=6, tm_yday=87, tm_isdst=0 -) ->>> time.gmtime(0) -time.struct_time( - tm_year=1970, tm_mon=1, tm_mday=1, - tm_hour=0, tm_min=0, tm_sec=0, - tm_wday=3, tm_yday=1, tm_isdst=0 -) ->>> time.localtime() -time.struct_time( - tm_year=2021, tm_mon=3, tm_mday=28, - tm_hour=15, tm_min=23, tm_sec=50, - tm_wday=6, tm_yday=87, tm_isdst=1 -) ->>> time.time() -1616941458.149149 +print(time.ctime()) +print(time.daylight) + +print(time.gmtime()) +print(time.gmtime(0)) +print(time.localtime()) +print(time.time()) # datetime, timezones and tiemdeltas ->>> now = datetime.now() ->>> utcnow = datetime.utcnow() ->>> now -datetime.datetime(2021, 3, 28, 15, 25, 16, 258274) ->>> utcnow -datetime.datetime(2021, 3, 28, 14, 25, 22, 918195) ->>> now.date() -datetime.date(2021, 3, 28) ->>> now.day, now.month, now.year -(28, 3, 2021) ->>> now.date() == date.today() -True ->>> now.time() -datetime.time(15, 25, 16, 258274) ->>> now.hour, now.minute, now.second, now.microsecond -(15, 25, 16, 258274) ->>> now.ctime() -'Sun Mar 28 15:25:16 2021' ->>> now.isoformat() -'2021-03-28T15:25:16.258274' ->>> now.timetuple() -time.struct_time( - tm_year=2021, tm_mon=3, tm_mday=28, - tm_hour=15, tm_min=25, tm_sec=16, - tm_wday=6, tm_yday=87, tm_isdst=-1 -) ->>> now.tzinfo ->>> utcnow.tzinfo ->>> now.weekday() -6 ->>> f_bday = datetime( - 1975, 12, 29, 12, 50, tzinfo=ZoneInfo('Europe/Rome') -) ->>> h_bday = datetime( - 1981, 10, 7, 15, 30, 50, tzinfo=timezone(timedelta(hours=2)) -) ->>> diff = h_bday - f_bday ->>> type(diff) - ->>> diff.days -2109 ->>> diff.total_seconds() -182223650.0 ->>> today + timedelta(days=49) -datetime.date(2021, 5, 16) ->>> now + timedelta(weeks=7) -datetime.datetime(2021, 5, 16, 15, 25, 16, 258274) +now = datetime.now() +print(now) + +utcnow = datetime.utcnow() +print(utcnow) + +print(now.date()) +print(now.day, now.month, now.year) +print(now.date() == date.today()) + +print(now.time()) +print(now.hour, now.minute, now.second, now.microsecond) + +print(now.ctime()) +print(now.isoformat()) + +print(now.timetuple()) +print(now.tzinfo) +print(utcnow.tzinfo) + +print(now.weekday()) +f_bday = datetime(1975, 12, 29, 12, 50, tzinfo=ZoneInfo("Europe/Rome")) +h_bday = datetime(1981, 10, 7, 15, 30, 50, tzinfo=timezone(timedelta(hours=2))) +diff = h_bday - f_bday +print(diff) +print(type(diff)) + +print(diff.days) +print(diff.total_seconds()) + +print(today + timedelta(days=49)) +print(now + timedelta(weeks=7)) # parsing (stdlib) ->>> datetime.fromisoformat('1977-11-24T19:30:13+01:00') -datetime.datetime( - 1977, 11, 24, 19, 30, 13, - tzinfo=datetime.timezone(datetime.timedelta(seconds=3600)) -) +print(datetime.fromisoformat("1977-11-24T19:30:13+01:00")) +print(datetime.fromtimestamp(time.time())) ->>> datetime.fromtimestamp(time.time()) -datetime.datetime(2021, 3, 28, 15, 42, 2, 142696) +print(datetime.now()) ->>> datetime.now() -datetime.datetime(2021, 3, 28, 15, 42, 1, 120094) +print(arrow.utcnow()) +print(arrow.now()) +local = arrow.now("Europe/Rome") +print(local) -# arrow small demo ->>> import arrow ->>> arrow.utcnow() - ->>> arrow.now() - - ->>> local = arrow.now('Europe/Rome') ->>> local - ->>> local.to('utc') - ->>> local.to('Europe/Moscow') - ->>> local.to('Asia/Tokyo') - ->>> local.datetime -datetime.datetime( - 2021, 3, 28, 16, 59, 14, 93960, - tzinfo=tzfile('/usr/share/zoneinfo/Europe/Rome') -) ->>> local.isoformat() -'2021-03-28T16:59:14.093960+02:00' +print(local.to("utc")) +print(local.to("Europe/Moscow")) +print(local.to("Asia/Tokyo")) + +print(local.datetime) +print(local.isoformat()) diff --git a/ch02/defaultdict.py b/ch02/defaultdict.py index cf5d931..6e2c51f 100644 --- a/ch02/defaultdict.py +++ b/ch02/defaultdict.py @@ -1,18 +1,14 @@ # defaultdict.py +from collections import defaultdict +d = {} +d["age"] = d.get("age", 0) + 1 # age not there, we get 0 + 1 +print(d) ->>> d = {} ->>> d['age'] = d.get('age', 0) + 1 # age not there, we get 0 + 1 ->>> d -{'age': 1} ->>> d = {'age': 39} ->>> d['age'] = d.get('age', 0) + 1 # age is there, we get 40 ->>> d -{'age': 40} +d = {"age": 39} +d["age"] = d.get("age", 0) + 1 # age is there, we get 40 +print(d) - ->>> from collections import defaultdict ->>> dd = defaultdict(int) # int is the default type (0 the value) ->>> dd['age'] += 1 # short for dd['age'] = dd['age'] + 1 ->>> dd -defaultdict(, {'age': 1}) # 1, as expected +dd = defaultdict(int) # int is the default type (0 the value) +dd["age"] += 1 # short for dd['age'] = dd['age'] + 1 +print(dd) diff --git a/ch02/dicts.py b/ch02/dicts.py index 264f4a2..36e4fdf 100644 --- a/ch02/dicts.py +++ b/ch02/dicts.py @@ -1,123 +1,93 @@ # dicts.py - ->>> a = dict(A=1, Z=-1) ->>> b = {'A': 1, 'Z': -1} ->>> c = dict(zip(['A', 'Z'], [1, -1])) ->>> d = dict([('A', 1), ('Z', -1)]) ->>> e = dict({'Z': -1, 'A': 1}) ->>> a == b == c == d == e # are they all the same? -True # They are indeed - +a = dict(A=1, Z=-1) +b = {"A": 1, "Z": -1} +c = dict(zip(["A", "Z"], [1, -1])) +d = dict([("A", 1), ("Z", -1)]) +e = dict({"Z": -1, "A": 1}) +print(a == b == c == d == e) # are they all the same? # zip ->>> list(zip(['h', 'e', 'l', 'l', 'o'], [1, 2, 3, 4, 5])) -[('h', 1), ('e', 2), ('l', 3), ('l', 4), ('o', 5)] ->>> list(zip('hello', range(1, 6))) # equivalent, more pythonic -[('h', 1), ('e', 2), ('l', 3), ('l', 4), ('o', 5)] - +print(list(zip(["h", "e", "l", "l", "o"], [1, 2, 3, 4, 5]))) +print(list(zip("hello", range(1, 6)))) # equivalent, more pythonic # basic ->>> d = {} ->>> d['a'] = 1 # let's set a couple of (key, value) pairs ->>> d['b'] = 2 ->>> len(d) # how many pairs? -2 ->>> d['a'] # what is the value of 'a'? -1 ->>> d # how does `d` look now? -{'a': 1, 'b': 2} ->>> del d['a'] # let's remove `a` ->>> d -{'b': 2} ->>> d['c'] = 3 # let's add 'c': 3 ->>> 'c' in d # membership is checked against the keys -True ->>> 3 in d # not the values -False ->>> 'e' in d -False ->>> d.clear() # let's clean everything from this dictionary ->>> d -{} +d = {} +d["a"] = 1 # let's set a couple of (key, value) pairs +d["b"] = 2 +print(len(d)) # how many pairs? + +print(d["a"]) # what is the value of 'a'? +print(d) # how does `d` look now? +del d["a"] # let's remove `a` +print(d) +d["c"] = 3 # let's add 'c': 3 +print("c" in d) # membership is checked against the keys +print(3 in d) # not the values +print("e" in d) + +d.clear() # let's clean everything from this dictionary +print(d) # views ->>> d = dict(zip('hello', range(5))) ->>> d -{'h': 0, 'e': 1, 'l': 3, 'o': 4} ->>> d.keys() -dict_keys(['h', 'e', 'l', 'o']) ->>> d.values() -dict_values([0, 1, 3, 4]) ->>> d.items() -dict_items([('h', 0), ('e', 1), ('l', 3), ('o', 4)]) ->>> 3 in d.values() -True ->>> ('o', 4) in d.items() -True +d = dict(zip("hello", range(5))) +print(d) + +print(d.keys()) +print(d.values()) +print(d.items()) +print(3 in d.values()) +print(("o", 4) in d.items()) # other methods ->>> d -{'h': 0, 'e': 1, 'l': 3, 'o': 4} ->>> d.popitem() # removes a random item (useful in algorithms) -('o', 4) ->>> d -{'h': 0, 'e': 1, 'l': 3} ->>> d.pop('l') # remove item with key `l` -3 ->>> d.pop('not-a-key') # remove a key not in dictionary: KeyError -Traceback (most recent call last): - File "", line 1, in -KeyError: 'not-a-key' ->>> d.pop('not-a-key', 'default-value') # with a default value? -'default-value' # we get the default value ->>> d.update({'another': 'value'}) # we can update dict this way ->>> d.update(a=13) # or this way (like a function call) ->>> d -{'h': 0, 'e': 1, 'another': 'value', 'a': 13} ->>> d.get('a') # same as d['a'] but if key is missing no KeyError -13 ->>> d.get('a', 177) # default value used if key is missing -13 ->>> d.get('b', 177) # like in this case -177 ->>> d.get('b') # key is not there, so None is returned +print(d) +print(d.popitem()) # removes a random item (useful in algorithms) + +print(d) +print(d.pop("l")) # remove item with key `l` +# print(d.pop('not-a-key')) # remove a key not in dictionary: KeyError +# Traceback (most recent call last): +# File "", line 1, in +# KeyError: 'not-a-key' +print(d.pop("not-a-key", "default-value")) # with a default value? + +print(d.update({"another": "value"})) # we can update dict this way +print(d.update(a=13)) # or this way (like a function call) +print(d) + +print(d.get("a")) # same as d['a'] but if key is missing no KeyError +print(d.get("a", 177)) # default value used if key is missing + +print(d.get("b", 177)) # like in this case +print(d.get("b")) # key is not there, so None is returned # setdefault ->>> d = {} ->>> d.setdefault('a', 1) # 'a' is missing, we get default value -1 ->>> d -{'a': 1} # also, the key/value pair ('a', 1) has now been added ->>> d.setdefault('a', 5) # let's try to override the value -1 ->>> d -{'a': 1} # no override, as expected +d = {} +print(d.setdefault("a", 1)) # 'a' is missing, we get default value +print(d) +# {'a': 1} # also, the key/value pair ('a', 1) has now been added +print(d.setdefault("a", 5)) # let's try to override the value +print(d) +# {'a': 1} # no override, as expected # setdefault example ->>> d = {} ->>> d.setdefault('a', {}).setdefault('b', []).append(1) ->>> d -{'a': {'b': [1]}} - +d = {} +d.setdefault("a", {}).setdefault("b", []).append(1) +print(d) # union ->>> d = {'a': 'A', 'b': 'B'} ->>> e = {'b': 8, 'c': 'C'} ->>> d | e -{'a': 'A', 'b': 8, 'c': 'C'} ->>> e | d -{'b': 'B', 'c': 'C', 'a': 'A'} ->>> {**d, **e} -{'a': 'A', 'b': 8, 'c': 'C'} ->>> {**e, **d} -{'b': 'B', 'c': 'C', 'a': 'A'} - ->>> d |= e ->>> d -{'a': 'A', 'b': 8, 'c': 'C'} +d = {"a": "A", "b": "B"} +e = {"b": 8, "c": "C"} +print(d | e) +print(e | d) + +print({**d, **e}) +print({**e, **d}) + +d |= e +print(d) diff --git a/ch02/enum.py b/ch02/enum.py index 842b4e2..1be2aeb 100644 --- a/ch02/enum.py +++ b/ch02/enum.py @@ -1,26 +1,24 @@ ->>> GREEN = 1 ->>> YELLOW = 2 ->>> RED = 4 ->>> TRAFFIC_LIGHTS = (GREEN, YELLOW, RED) ->>> # or with a dict ->>> traffic_lights = {'GREEN': 1, 'YELLOW': 2, 'RED': 4} +# using enum +from enum import Enum +GREEN = 1 +YELLOW = 2 +RED = 4 +TRAFFIC_LIGHTS = (GREEN, YELLOW, RED) +# or with a dict +traffic_lights = {"GREEN": 1, "YELLOW": 2, "RED": 4} -# using enum ->>> from enum import Enum ->>> class TrafficLight(Enum): -... GREEN = 1 -... YELLOW = 2 -... RED = 4 -... ->>> TrafficLight.GREEN - ->>> TrafficLight.GREEN.name -'GREEN' ->>> TrafficLight.GREEN.value -1 ->>> TrafficLight(1) - ->>> TrafficLight(4) - + +class TrafficLight(Enum): + GREEN = 1 + YELLOW = 2 + RED = 4 + + +print(TrafficLight.GREEN) +print(TrafficLight.GREEN.name) + +print(TrafficLight.GREEN.value) +print(TrafficLight(1)) +print(TrafficLight(4)) diff --git a/ch02/final_considerations.py b/ch02/final_considerations.py index bfaaacc..c4211cb 100644 --- a/ch02/final_considerations.py +++ b/ch02/final_considerations.py @@ -1,46 +1,39 @@ # final_considerations.py +a = 1000000 +b = 1000000 +print(id(a) == id(b)) - ->>> a = 1000000 ->>> b = 1000000 ->>> id(a) == id(b) -False - - ->>> a = 5 ->>> b = 5 ->>> id(a) == id(b) -True - +a = 5 +b = 5 +print(id(a) == id(b)) # how to choose data structures # example customer objects -customer1 = {'id': 'abc123', 'full_name': 'Master Yoda'} -customer2 = {'id': 'def456', 'full_name': 'Obi-Wan Kenobi'} -customer3 = {'id': 'ghi789', 'full_name': 'Anakin Skywalker'} +customer1 = {"id": "abc123", "full_name": "Master Yoda"} +customer2 = {"id": "def456", "full_name": "Obi-Wan Kenobi"} +customer3 = {"id": "ghi789", "full_name": "Anakin Skywalker"} + # collect them in a tuple customers = (customer1, customer2, customer3) + # or collect them in a list customers = [customer1, customer2, customer3] + # or maybe within a dictionary, they have a unique id after all customers = { - 'abc123': customer1, - 'def456': customer2, - 'ghi789': customer3, + "abc123": customer1, + "def456": customer2, + "ghi789": customer3, } - # negative indexing ->>> a = list(range(10)) # `a` has 10 elements. Last one is 9. ->>> a -[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] ->>> len(a) # its length is 10 elements -10 ->>> a[len(a) - 1] # position of last one is len(a) - 1 -9 ->>> a[-1] # but we don't need len(a)! Python rocks! -9 ->>> a[-2] # equivalent to len(a) - 2 -8 ->>> a[-3] # equivalent to len(a) - 3 -7 +a = list(range(10)) # `a` has 10 elements. Last one is 9. +print(a) + +print(len(a)) # its length is 10 elements +print(a[len(a) - 1]) # position of last one is len(a) - 1 + +print(a[-1]) # but we don't need len(a)! Python rocks! + +print(a[-2]) # equivalent to len(a) - 2 +print(a[-3]) # equivalent to len(a) - 3 diff --git a/ch02/lists.py b/ch02/lists.py index 30d201c..5e6cd6f 100644 --- a/ch02/lists.py +++ b/ch02/lists.py @@ -1,101 +1,84 @@ # lists.py - +from math import prod +from operator import itemgetter # creation ->>> [] # empty list -[] ->>> list() # same as [] -[] ->>> [1, 2, 3] # as with tuples, items are comma separated -[1, 2, 3] ->>> [x + 5 for x in [2, 3, 4]] # Python is magic -[7, 8, 9] ->>> list((1, 3, 5, 7, 9)) # list from a tuple -[1, 3, 5, 7, 9] ->>> list('hello') # list from a string -['h', 'e', 'l', 'l', 'o'] +print([]) # empty list +print(list()) # same as [] +print([1, 2, 3]) # as with tuples, items are comma separated +print([x + 5 for x in [2, 3, 4]]) # Python is magic +print(list((1, 3, 5, 7, 9))) # list from a tuple +print(list("hello")) # list from a string # main methods ->>> a = [1, 2, 1, 3] ->>> a.append(13) # we can append anything at the end ->>> a -[1, 2, 1, 3, 13] ->>> a.count(1) # how many `1` are there in the list? -2 ->>> a.extend([5, 7]) # extend the list by another (or sequence) ->>> a -[1, 2, 1, 3, 13, 5, 7] ->>> a.index(13) # position of `13` in the list (0-based indexing) -4 ->>> a.insert(0, 17) # insert `17` at position 0 ->>> a -[17, 1, 2, 1, 3, 13, 5, 7] ->>> a.pop() # pop (remove and return) last element -7 ->>> a.pop(3) # pop element at position 3 -1 ->>> a -[17, 1, 2, 3, 13, 5] ->>> a.remove(17) # remove `17` from the list ->>> a -[1, 2, 3, 13, 5] ->>> a.reverse() # reverse the order of the elements in the list ->>> a -[5, 13, 3, 2, 1] ->>> a.sort() # sort the list ->>> a -[1, 2, 3, 5, 13] ->>> a.clear() # remove all elements from the list ->>> a -[] +a = [1, 2, 1, 3] +a.append(13) # we can append anything at the end +print(a) + +print(a.count(1)) # how many `1` are there in the list? + +a.extend([5, 7]) # extend the list by another (or sequence) +print(a) + +print(a.index(13)) # position of `13` in the list (0-based indexing) + +a.insert(0, 17) # insert `17` at position 0 +print(a) + +a.pop() # pop (remove and return) last element +print(a) + +a.pop(3) # pop element at position 3 +print(a) + +a.remove(17) # remove `17` from the list +print(a) + +a.reverse() # reverse the order of the elements in the list +print(a) +a.sort() # sort the list +print(a) + +a.clear() # remove all elements from the list +print(a) # extending ->>> a = list('hello') # makes a list from a string ->>> a -['h', 'e', 'l', 'l', 'o'] ->>> a.append(100) # append 100, heterogeneous type ->>> a -['h', 'e', 'l', 'l', 'o', 100] ->>> a.extend((1, 2, 3)) # extend using tuple ->>> a -['h', 'e', 'l', 'l', 'o', 100, 1, 2, 3] ->>> a.extend('...') # extend using string ->>> a -['h', 'e', 'l', 'l', 'o', 100, 1, 2, 3, '.', '.', '.'] +a = list("hello") # makes a list from a string +print(a) + +a.append(100) # append 100, heterogeneous type +print(a) + +a.extend((1, 2, 3)) # extend using tuple +print(a) +a.extend("...") # extend using string +print(a) # most common operations ->>> a = [1, 3, 5, 7] ->>> min(a) # minimum value in the list -1 ->>> max(a) # maximum value in the list -7 ->>> sum(a) # sum of all values in the list -16 ->>> from math import prod ->>> prod(a) # product of all values in the list -105 ->>> len(a) # number of elements in the list -4 ->>> b = [6, 7, 8] ->>> a + b # `+` with list means concatenation -[1, 3, 5, 7, 6, 7, 8] ->>> a * 2 # `*` has also a special meaning -[1, 3, 5, 7, 1, 3, 5, 7] +a = [1, 3, 5, 7] +print(min(a)) # minimum value in the list + +print(max(a)) # maximum value in the list + +print(sum(a)) # sum of all values in the list +print(prod(a)) # product of all values in the list + +print(len(a)) # number of elements in the list + +b = [6, 7, 8] +print(a + b) # `+` with list means concatenation + +print(a * 2) # `*` has also a special meaning # cool sorting ->>> from operator import itemgetter ->>> a = [(5, 3), (1, 3), (1, 2), (2, -1), (4, 9)] ->>> sorted(a) -[(1, 2), (1, 3), (2, -1), (4, 9), (5, 3)] ->>> sorted(a, key=itemgetter(0)) -[(1, 3), (1, 2), (2, -1), (4, 9), (5, 3)] ->>> sorted(a, key=itemgetter(0, 1)) -[(1, 2), (1, 3), (2, -1), (4, 9), (5, 3)] ->>> sorted(a, key=itemgetter(1)) -[(2, -1), (1, 2), (5, 3), (1, 3), (4, 9)] ->>> sorted(a, key=itemgetter(1), reverse=True) -[(4, 9), (5, 3), (1, 3), (1, 2), (2, -1)] +a = [(5, 3), (1, 3), (1, 2), (2, -1), (4, 9)] +print(sorted(a)) + +print(sorted(a, key=itemgetter(0))) +print(sorted(a, key=itemgetter(0, 1))) +print(sorted(a, key=itemgetter(1))) +print(sorted(a, key=itemgetter(1), reverse=True)) diff --git a/ch02/namedtuple.py b/ch02/namedtuple.py index 2b38a95..2e5f24f 100644 --- a/ch02/namedtuple.py +++ b/ch02/namedtuple.py @@ -1,34 +1,23 @@ # namedtuple.py - +from collections import namedtuple # the problem ->>> vision = (9.5, 8.8) ->>> vision -(9.5, 8.8) ->>> vision[0] # left eye (implicit positional reference) -9.5 ->>> vision[1] # right eye (implicit positional reference) -8.8 +vision = (9.5, 8.8) +print(vision) +print(vision[0]) # left eye (implicit positional reference) +print(vision[1]) # right eye (implicit positional reference) # the solution ->>> from collections import namedtuple ->>> Vision = namedtuple('Vision', ['left', 'right']) ->>> vision = Vision(9.5, 8.8) ->>> vision[0] -9.5 ->>> vision.left # same as vision[0], but explicit -9.5 ->>> vision.right # same as vision[1], but explicit -8.8 - +Vision = namedtuple("Vision", ["left", "right"]) +vision = Vision(9.5, 8.8) +print(vision[0]) +print(vision.left) # same as vision[0], but explicit +print(vision.right) # same as vision[1], but explicit # the change ->>> Vision = namedtuple('Vision', ['left', 'combined', 'right']) ->>> vision = Vision(9.5, 9.2, 8.8) ->>> vision.left # still correct -9.5 ->>> vision.right # still correct (though now is vision[2]) -8.8 ->>> vision.combined # the new vision[1] -9.2 +Vision = namedtuple("Vision", ["left", "combined", "right"]) +vision = Vision(9.5, 9.2, 8.8) +print(vision.left) # still correct +print(vision.right) # still correct (though now is vision[2]) +print(vision.combined) # the new vision[1] diff --git a/ch02/numbers.py b/ch02/numbers.py index 8af2741..0fc2ee2 100644 --- a/ch02/numbers.py +++ b/ch02/numbers.py @@ -1,180 +1,158 @@ # numbers.py - +from math import pow # integers ->>> a = 14 ->>> b = 3 ->>> a + b # addition -17 ->>> a - b # subtraction -11 ->>> a * b # multiplication -42 ->>> a / b # true division -4.666666666666667 ->>> a // b # integer division -4 ->>> a % b # modulo operation (reminder of division) -2 ->>> a ** b # power operation -2744 - ->>> from math import pow - ->>> pow(10, 3) -1000 ->>> 10 ** 3 -1000 ->>> pow(10, -3) -0.001 ->>> 10 ** -3 -0.001 - - ->>> pow(123, 4) -228886641 ->>> pow(123, 4, 100) -41 # notice: 228886641 % 100 == 41 ->>> pow(37, -1, 43) # modular inverse of 37 mod 43 -7 ->>> 7 * 37 % 43 # proof the above is correct -1 - - +a = 14 +b = 3 +print(a + b) # addition +print(a - b) # subtraction +print(a * b) # multiplication +print(a / b) # true division +print(a // b) # integer division +print(a % b) # modulo operation (reminder of division) +print(a**b) # power operation +print(pow(10, 3)) +print(10**3) +print(pow(10, -3)) +print(10**-3) +print(pow(123, 4)) +print(pow(123, 4, 100)) +# 41 # notice: 228886641 % 100 == 41 + +print(pow(37, -1, 43)) # modular inverse of 37 mod 43 +# 7 + +print(7 * 37 % 43) # proof the above is correct +# 1 # integer and true division ->>> 7 / 4 # true division -1.75 ->>> 7 // 4 # integer division, truncation returns 1 -1 ->>> -7 / 4 # true division again, result is opposite of previous --1.75 ->>> -7 // 4 # integer div., result not the opposite of previous --2 +print(7 / 4) # true division +print(7 // 4) # integer division, truncation returns 1 +print(-7 / 4) # true division again, result is opposite of previous +print(-7 // 4) # integer div., result not the opposite of previous # modulo operator ->>> 10 % 3 # remainder of the division 10 // 3 -1 ->>> 10 % 4 # remainder of the division 10 // 4 -2 - - -# truncation towards 0 ->>> int(1.75) -1 ->>> int(-1.75) --1 - -# creating ints ->>> int('10110', base=2) -22 - - -# underscored ->>> n = 1_024 ->>> n -1024 ->>> hex_n = 0x_4_0_0 # 0x400 == 1024 ->>> hex_n -1024 - - -# booleans ->>> int(True) # True behaves like 1 -1 ->>> int(False) # False behaves like 0 -0 ->>> bool(1) # 1 evaluates to True in a boolean context -True ->>> bool(-42) # and so does every non-zero number -True ->>> bool(0) # 0 evaluates to False -False ->>> # quick peak at the operators (and, or, not) ->>> not True -False ->>> not False -True ->>> True and True -True ->>> False or True -True - - -# int and bool ->>> 1 + True -2 ->>> False + 42 -42 ->>> 7 - True -6 - - -# reals ->>> pi = 3.1415926536 # how many digits of PI can you remember? ->>> radius = 4.5 ->>> area = pi * (radius ** 2) ->>> area -63.617251235400005 - - -# sys.float_info ->>> import sys ->>> sys.float_info -sys.float_info( - max=1.7976931348623157e+308, max_exp=1024, max_10_exp=308, - min=2.2250738585072014e-308, min_exp=-1021, min_10_exp=-307, - dig=15, mant_dig=53, epsilon=2.220446049250313e-16, radix=2, - rounds=1 -) - -# approximation issue ->>> 0.3 - 0.1 * 3 # this should be 0!!! --5.551115123125783e-17 - - -# complex ->>> c = 3.14 + 2.73j ->>> c = complex(3.14, 2.73) # same as above ->>> c.real # real part -3.14 ->>> c.imag # imaginary part -2.73 ->>> c.conjugate() # conjugate of A + Bj is A - Bj -(3.14-2.73j) ->>> c * 2 # multiplication is allowed -(6.28+5.46j) ->>> c ** 2 # power operation as well -(2.4067000000000007+17.1444j) ->>> d = 1 + 1j # addition and subtraction as well ->>> c - d -(2.14+1.73j) - - -# fractions ->>> from fractions import Fraction ->>> Fraction(10, 6) # mad hatter? -Fraction(5, 3) # notice it's been simplified ->>> Fraction(1, 3) + Fraction(2, 3) # 1/3 + 2/3 == 3/3 == 1/1 -Fraction(1, 1) ->>> f = Fraction(10, 6) ->>> f.numerator -5 ->>> f.denominator -3 ->>> f.as_integer_ratio() -(5, 3) - - -# decimal ->>> from decimal import Decimal as D # rename for brevity ->>> D(3.14) # pi, from float, so approximation issues -Decimal('3.140000000000000124344978758017532527446746826171875') ->>> D('3.14') # pi, from a string, so no approximation issues -Decimal('3.14') ->>> D(0.1) * D(3) - D(0.3) # from float, we still have the issue -Decimal('2.775557561565156540423631668E-17') ->>> D('0.1') * D(3) - D('0.3') # from string, all perfect -Decimal('0.0') ->>> D('1.4').as_integer_ratio() # 7/5 = 1.4 (isn't this cool?!) -(7, 5) +print(10 % 3) # remainder of the division 10 // 3 +# >>> 10 % 4 # remainder of the division 10 // 4 +# 2 + + +# # truncation towards 0 +# >>> int(1.75) +# 1 +# >>> int(-1.75) +# -1 + +# # creating ints +# >>> int('10110', base=2) +# 22 + + +# # underscored +# >>> n = 1_024 +# >>> n +# 1024 +# >>> hex_n = 0x_4_0_0 # 0x400 == 1024 +# >>> hex_n +# 1024 + + +# # booleans +# >>> int(True) # True behaves like 1 +# 1 +# >>> int(False) # False behaves like 0 +# 0 +# >>> bool(1) # 1 evaluates to True in a boolean context +# True +# >>> bool(-42) # and so does every non-zero number +# True +# >>> bool(0) # 0 evaluates to False +# False +# >>> # quick peak at the operators (and, or, not) +# >>> not True +# False +# >>> not False +# True +# >>> True and True +# True +# >>> False or True +# True + + +# # int and bool +# >>> 1 + True +# 2 +# >>> False + 42 +# 42 +# >>> 7 - True +# 6 + + +# # reals +# >>> pi = 3.1415926536 # how many digits of PI can you remember? +# >>> radius = 4.5 +# >>> area = pi * (radius ** 2) +# >>> area +# 63.617251235400005 + + +# # sys.float_info +# >>> import sys +# >>> sys.float_info +# sys.float_info( +# max=1.7976931348623157e+308, max_exp=1024, max_10_exp=308, +# min=2.2250738585072014e-308, min_exp=-1021, min_10_exp=-307, +# dig=15, mant_dig=53, epsilon=2.220446049250313e-16, radix=2, +# rounds=1 +# ) + +# # approximation issue +# >>> 0.3 - 0.1 * 3 # this should be 0!!! +# -5.551115123125783e-17 + + +# # complex +# >>> c = 3.14 + 2.73j +# >>> c = complex(3.14, 2.73) # same as above +# >>> c.real # real part +# 3.14 +# >>> c.imag # imaginary part +# 2.73 +# >>> c.conjugate() # conjugate of A + Bj is A - Bj +# (3.14-2.73j) +# >>> c * 2 # multiplication is allowed +# (6.28+5.46j) +# >>> c ** 2 # power operation as well +# (2.4067000000000007+17.1444j) +# >>> d = 1 + 1j # addition and subtraction as well +# >>> c - d +# (2.14+1.73j) + + +# # fractions +# >>> from fractions import Fraction +# >>> Fraction(10, 6) # mad hatter? +# Fraction(5, 3) # notice it's been simplified +# >>> Fraction(1, 3) + Fraction(2, 3) # 1/3 + 2/3 == 3/3 == 1/1 +# Fraction(1, 1) +# >>> f = Fraction(10, 6) +# >>> f.numerator +# 5 +# >>> f.denominator +# 3 +# >>> f.as_integer_ratio() +# (5, 3) + + +# # decimal +# >>> from decimal import Decimal as D # rename for brevity +# >>> D(3.14) # pi, from float, so approximation issues +# Decimal('3.140000000000000124344978758017532527446746826171875') +# >>> D('3.14') # pi, from a string, so no approximation issues +# Decimal('3.14') +# >>> D(0.1) * D(3) - D(0.3) # from float, we still have the issue +# Decimal('2.775557561565156540423631668E-17') +# >>> D('0.1') * D(3) - D('0.3') # from string, all perfect +# Decimal('0.0') +# >>> D('1.4').as_integer_ratio() # 7/5 = 1.4 (isn't this cool?!) +# (7, 5) diff --git a/ch02/objects.py b/ch02/objects.py index 014c9ee..356112c 100644 --- a/ch02/objects.py +++ b/ch02/objects.py @@ -1,37 +1,31 @@ # objects.py # code block # 1 ->>> age = 42 ->>> age -42 ->>> age = 43 #A ->>> age -43 +age = 42 +print(age) +age = 43 # A +print(age) # code block # 2 ->>> age = 42 ->>> id(age) -4377553168 ->>> age = 43 ->>> id(age) -4377553200 +age = 42 +print(id(age)) + +age = 43 +print(id(age)) # code block # 3 ->>> class Person: -... def __init__(self, age): -... self.age = age -... ->>> fab = Person(age=42) ->>> fab.age -42 ->>> id(fab) -4380878496 ->>> id(fab.age) -4377553168 ->>> fab.age = 25 # I wish! ->>> id(fab) # will be the same -4380878496 ->>> id(fab.age) # will be different -4377552624 +class Person: + def __init__(self, age): + self.age = age + + +fab = Person(age=42) +print(fab.age) +print(id(fab)) +print(id(fab.age)) + +fab.age = 25 # I wish! +print(id(fab)) # will be the same +print(id(fab.age)) # will be different diff --git a/ch02/requirements/main.in b/ch02/requirements/main.in deleted file mode 100644 index e2dc747..0000000 --- a/ch02/requirements/main.in +++ /dev/null @@ -1 +0,0 @@ -arrow diff --git a/ch02/requirements/main.txt b/ch02/requirements/main.txt deleted file mode 100644 index 55137d9..0000000 --- a/ch02/requirements/main.txt +++ /dev/null @@ -1,12 +0,0 @@ -# -# This file is autogenerated by pip-compile -# To update, run: -# -# pip-compile main.in -# -arrow==1.0.3 - # via -r main.in -python-dateutil==2.8.1 - # via arrow -six==1.15.0 - # via python-dateutil diff --git a/ch02/sequences.py b/ch02/sequences.py index c46fe78..188977d 100644 --- a/ch02/sequences.py +++ b/ch02/sequences.py @@ -1,103 +1,111 @@ # sequences.py - +from math import pi # strings ->>> # 4 ways to make a string ->>> str1 = 'This is a string. We built it with single quotes.' ->>> str2 = "This is also a string, but built with double quotes." ->>> str3 = '''This is built using triple quotes, -... so it can span multiple lines.''' ->>> str4 = """This too -... is a multiline one -... built with triple double-quotes.""" ->>> str4 #A -'This too\nis a multiline one\nbuilt with triple double-quotes.' ->>> print(str4) #B -This too +# 4 ways to make a string +str1 = "This is a string. We built it with single quotes." +str2 = "This is also a string, but built with double quotes." + +str3 = """This is built using triple quotes, +so it can span multiple lines.""" + +str4 = """This too is a multiline one -built with triple double-quotes. +built with triple double-quotes.""" ->>> s = 'Hello There' ->>> s.removeprefix('Hell') -'o There' ->>> s.removesuffix('here') -'Hello T' ->>> s.removeprefix('Ooops') -'Hello There' +print(str3) # A +print(str4) # B +s = "Hello There" +print(s.removeprefix("Hell")) +print(s.removesuffix("here")) +print(s.removeprefix("Ooops")) # encode / decode ->>> s = "This is üŋíc0de" # unicode string: code points ->>> type(s) - ->>> encoded_s = s.encode('utf-8') # utf-8 encoded version of s ->>> encoded_s -b'This is \xc3\xbc\xc5\x8b\xc3\xadc0de' # result: bytes object ->>> type(encoded_s) # another way to verify it - ->>> encoded_s.decode('utf-8') # let's revert to the original -'This is üŋíc0de' ->>> bytes_obj = b"A bytes object" # a bytes object ->>> type(bytes_obj) - +s = "This is üŋíc0de" # unicode string: code points +print(type(s)) +encoded_s = s.encode("utf-8") # utf-8 encoded version of s +print(encoded_s) +# b'This is \xc3\xbc\xc5\x8b\xc3\xadc0de' # result: bytes object -# length ->>> len(str1) -49 +print(type(encoded_s)) # another way to verify it +# + +decoded_s = encoded_s.decode("utf-8") # let's revert to the original +print(decoded_s) +# 'This is üŋíc0de' +bytes_obj = b"A bytes object" # a bytes object +print(type(bytes_obj)) +# + +# length +print(len(str1)) +# 49 # indexing and slicing ->>> s = "The trouble is you think you have time." ->>> s[0] # indexing at position 0, which is the first char -'T' ->>> s[5] # indexing at position 5, which is the sixth char -'r' ->>> s[:4] # slicing, we specify only the stop position -'The ' ->>> s[4:] # slicing, we specify only the start position -'trouble is you think you have time.' ->>> s[2:14] # slicing, both start and stop positions -'e trouble is' ->>> s[2:14:3] # slicing, start, stop and step (every 3 chars) -'erb ' ->>> s[:] # quick way of making a copy -'The trouble is you think you have time.' +s = "The trouble is you think you have time." +print(s[0]) # indexing at position 0, which is the first char +# 'T' + +print(s[5]) # indexing at position 5, which is the sixth char +# 'r' + +print(s[:4]) # slicing, we specify only the stop position +# 'The ' + +print(s[4:]) # slicing, we specify only the start position +# 'trouble is you think you have time.' + +print(s[2:14]) # slicing, both start and stop positions +# 'e trouble is' + +print(s[2:14:3]) # slicing, start, stop and step (every 3 chars) +# 'erb ' +print(s[:]) # quick way of making a copy +# 'The trouble is you think you have time.' # formatting ->>> greet_old = 'Hello %s!' ->>> greet_old % 'Fabrizio' -'Hello Fabrizio!' ->>> greet_positional = 'Hello {}!' ->>> greet_positional.format('Fabrizio') -'Hello Fabrizio!' ->>> greet_positional = 'Hello {} {}!' ->>> greet_positional.format('Fabrizio', 'Romano') -'Hello Fabrizio Romano!' ->>> greet_positional_idx = 'This is {0}! {1} loves {0}!' ->>> greet_positional_idx.format('Python', 'Heinrich') -'This is Python! Heinrich loves Python!' ->>> greet_positional_idx.format('Coffee', 'Fab') -'This is Coffee! Fab loves Coffee!' ->>> keyword = 'Hello, my name is {name} {last_name}' ->>> keyword.format(name='Fabrizio', last_name='Romano') -'Hello, my name is Fabrizio Romano' +greet_old = "Hello %s!" +print(greet_old % "Fabrizio") +# 'Hello Fabrizio!' + +greet_positional = "Hello {}!" +print(greet_positional.format("Fabrizio")) +# 'Hello Fabrizio!' + +greet_positional = "Hello {} {}!" +print(greet_positional.format("Fabrizio", "Romano")) +# 'Hello Fabrizio Romano!' + +greet_positional_idx = "This is {0}! {1} loves {0}!" +print(greet_positional_idx.format("Python", "Heinrich")) +# 'This is Python! Heinrich loves Python!' + +print(greet_positional_idx.format("Coffee", "Fab")) +# 'This is Coffee! Fab loves Coffee!' + +keyword = "Hello, my name is {name} {last_name}" +print(keyword.format(name="Fabrizio", last_name="Romano")) +# 'Hello, my name is Fabrizio Romano' # formatted string literals ->>> name = 'Fab' ->>> age = 42 ->>> f"Hello! My name is {name} and I'm {age}" -"Hello! My name is Fab and I'm 42" ->>> from math import pi ->>> f"No arguing with {pi}, it's irrational..." -"No arguing with 3.141592653589793, it's irrational..." +name = "Fab" +age = 42 +print(f"Hello! My name is {name} and I'm {age}") +# "Hello! My name is Fab and I'm 42" + +print(f"No arguing with {pi}, it's irrational...") +# "No arguing with 3.141592653589793, it's irrational..." # f-string debug ->>> user = 'heinrich' ->>> password = 'super-secret' ->>> f"Log in with: {user} and {password}" -'Log in with: heinrich and super-secret' ->>> f"Log in with: {user=} and {password=}" -"Log in with: user='heinrich' and password='super-secret'" +user = "heinrich" +password = "super-secret" +print(f"Log in with: {user} and {password}") +# 'Log in with: heinrich and super-secret' + +print(f"Log in with: {user=} and {password=}") +# "Log in with: user='heinrich' and password='super-secret'" diff --git a/ch02/sets.py b/ch02/sets.py index 1f8eb30..af41da4 100644 --- a/ch02/sets.py +++ b/ch02/sets.py @@ -1,51 +1,56 @@ # sets.py +small_primes = set() # empty set +small_primes.add(2) # adding one element at a time +small_primes.add(3) +small_primes.add(5) +print(small_primes) +# {2, 3, 5} ->>> small_primes = set() # empty set ->>> small_primes.add(2) # adding one element at a time ->>> small_primes.add(3) ->>> small_primes.add(5) ->>> small_primes -{2, 3, 5} ->>> small_primes.add(1) # Look what I've done, 1 is not a prime! ->>> small_primes -{1, 2, 3, 5} ->>> small_primes.remove(1) # so let's remove it ->>> 3 in small_primes # membership test -True ->>> 4 in small_primes -False ->>> 4 not in small_primes # negated membership test -True ->>> small_primes.add(3) # trying to add 3 again ->>> small_primes -{2, 3, 5} # no change, duplication is not allowed ->>> bigger_primes = set([5, 7, 11, 13]) # faster creation ->>> small_primes | bigger_primes # union operator `|` -{2, 3, 5, 7, 11, 13} ->>> small_primes & bigger_primes # intersection operator `&` -{5} ->>> small_primes - bigger_primes # difference operator `-` -{2, 3} - - ->>> small_primes = {2, 3, 5, 5, 3} ->>> small_primes -{2, 3, 5} +small_primes.add(1) # Look what I've done, 1 is not a prime! +print(small_primes) +# {1, 2, 3, 5} +small_primes.remove(1) # so let's remove it +print(3 in small_primes) # membership test +# True +print(4 in small_primes) +# False +print(4 not in small_primes) # negated membership test +# True + +small_primes.add(3) # trying to add 3 again +print(small_primes) +# {2, 3, 5} # no change, duplication is not allowed + +bigger_primes = set([5, 7, 11, 13]) # faster creation +print(small_primes | bigger_primes) # union operator `|` +# {2, 3, 5, 7, 11, 13} + +print(small_primes & bigger_primes) # intersection operator `&` +# {5} + +print(small_primes - bigger_primes) # difference operator `-` +# {2, 3} + +small_primes = {2, 3, 5, 5, 3} +print(small_primes) +# {2, 3, 5} # frozenset ->>> small_primes = frozenset([2, 3, 5, 7]) ->>> bigger_primes = frozenset([5, 7, 11]) ->>> small_primes.add(11) # we cannot add to a frozenset -Traceback (most recent call last): - File "", line 1, in -AttributeError: 'frozenset' object has no attribute 'add' ->>> small_primes.remove(2) # neither we can remove -Traceback (most recent call last): - File "", line 1, in -AttributeError: 'frozenset' object has no attribute 'remove' ->>> small_primes & bigger_primes # intersect, union, etc. allowed -frozenset({5, 7}) +small_primes = frozenset([2, 3, 5, 7]) +bigger_primes = frozenset([5, 7, 11]) +# small_primes.add(11) # we cannot add to a frozenset +# Traceback (most recent call last): +# File "", line 1, in +# AttributeError: 'frozenset' object has no attribute 'add' + +# small_primes.remove(2) # neither we can remove +# Traceback (most recent call last): +# File "", line 1, in +# AttributeError: 'frozenset' object has no attribute 'remove' + +print(small_primes & bigger_primes) # intersect, union, etc. allowed +# frozenset({5, 7}) diff --git a/ch02/tuples.py b/ch02/tuples.py index 13fc644..1beac5c 100644 --- a/ch02/tuples.py +++ b/ch02/tuples.py @@ -1,29 +1,30 @@ # tuples.py +t = () # empty tuple +print(type(t)) +# ->>> t = () # empty tuple ->>> type(t) - ->>> one_element_tuple = (42, ) # you need the comma! ->>> three_elements_tuple = (1, 3, 5) # braces are optional here ->>> a, b, c = 1, 2, 3 # tuple for multiple assignment ->>> a, b, c # implicit tuple to print with one instruction -(1, 2, 3) ->>> 3 in three_elements_tuple # membership test -True +one_element_tuple = (42,) # you need the comma! +three_elements_tuple = (1, 3, 5) # braces are optional here +a, b, c = 1, 2, 3 # tuple for multiple assignment +print(a, b, c) # implicit tuple to print with one instruction +# (1, 2, 3) + +print(3 in three_elements_tuple) # membership test +# True # swap ->>> a, b = 1, 2 ->>> c = a # we need three lines and a temporary var c ->>> a = b ->>> b = c ->>> a, b # a and b have been swapped -(2, 1) +a, b = 1, 2 +c = a # we need three lines and a temporary var c +a = b +b = c +print(a, b) # a and b have been swapped +# (2, 1) # pythonic swap ->>> a, b = 0, 1 ->>> a, b = b, a # this is the Pythonic way to do it ->>> a, b -(1, 0) +a, b = 0, 1 +a, b = b, a # this is the Pythonic way to do it +print(a, b) +# (1, 0) diff --git a/ch03/any.py b/ch03/any.py index bef25c5..13e0068 100644 --- a/ch03/any.py +++ b/ch03/any.py @@ -2,12 +2,12 @@ found = False # this is called "flag" for item in items: - print('scanning item', item) + print("scanning item", item) if item: found = True # we update the flag break if found: # we inspect the flag - print('At least one item evaluates to True') + print("At least one item evaluates to True") else: - print('All items evaluate to False') + print("All items evaluate to False") diff --git a/ch03/binary.py b/ch03/binary.1.py similarity index 100% rename from ch03/binary.py rename to ch03/binary.1.py diff --git a/ch03/compress.py b/ch03/compress.py index d49d3c7..ffc4306 100644 --- a/ch03/compress.py +++ b/ch03/compress.py @@ -1,4 +1,5 @@ from itertools import compress + data = range(10) even_selector = [1, 0] * 10 odd_selector = [0, 1] * 10 @@ -6,7 +7,10 @@ even_numbers = list(compress(data, even_selector)) odd_numbers = list(compress(data, odd_selector)) +print(even_selector) print(odd_selector) + print(list(data)) + print(even_numbers) print(odd_numbers) diff --git a/ch03/conditional.1.py b/ch03/conditional.1.py index 29dd5ed..b592492 100644 --- a/ch03/conditional.1.py +++ b/ch03/conditional.1.py @@ -1,3 +1,3 @@ late = True if late: - print('I need to call my manager!') + print("I need to call my manager!") diff --git a/ch03/conditional.2.py b/ch03/conditional.2.py index 30bebf9..cabf939 100644 --- a/ch03/conditional.2.py +++ b/ch03/conditional.2.py @@ -1,5 +1,5 @@ late = False if late: - print('I need to call my manager!') #1 + print("I need to call my manager!") # 1 else: - print('no need to call my manager...') #2 + print("no need to call my manager...") # 2 diff --git a/ch03/coupons.dict.py b/ch03/coupons.dict.py index cd98791..ac77a12 100644 --- a/ch03/coupons.dict.py +++ b/ch03/coupons.dict.py @@ -1,19 +1,21 @@ customers = [ - dict(id=1, total=200, coupon_code='F20'), # F20: fixed, £20 - dict(id=2, total=150, coupon_code='P30'), # P30: percent, 30% - dict(id=3, total=100, coupon_code='P50'), # P50: percent, 50% - dict(id=4, total=110, coupon_code='F15'), # F15: fixed, £15 + dict(id=1, total=200, coupon_code="F20"), # F20: fixed, £20 + dict(id=2, total=150, coupon_code="P30"), # P30: percent, 30% + dict(id=3, total=100, coupon_code="P50"), # P50: percent, 50% + dict(id=4, total=110, coupon_code="F15"), # F15: fixed, £15 ] + discounts = { - 'F20': (0.0, 20.0), # each value is (percent, fixed) - 'P30': (0.3, 0.0), - 'P50': (0.5, 0.0), - 'F15': (0.0, 15.0), + "F20": (0.0, 20.0), # each value is (percent, fixed) + "P30": (0.3, 0.0), + "P50": (0.5, 0.0), + "F15": (0.0, 15.0), } + for customer in customers: - code = customer['coupon_code'] + code = customer["coupon_code"] percent, fixed = discounts.get(code, (0.0, 0.0)) - customer['discount'] = percent * customer['total'] + fixed + customer["discount"] = percent * customer["total"] + fixed for customer in customers: - print(customer['id'], customer['total'], customer['discount']) + print(customer["id"], customer["total"], customer["discount"]) diff --git a/ch03/coupons.py b/ch03/coupons.py index eb7fd81..6dbdd47 100644 --- a/ch03/coupons.py +++ b/ch03/coupons.py @@ -1,21 +1,22 @@ customers = [ - dict(id=1, total=200, coupon_code='F20'), # F20: fixed, £20 - dict(id=2, total=150, coupon_code='P30'), # P30: percent, 30% - dict(id=3, total=100, coupon_code='P50'), # P50: percent, 50% - dict(id=4, total=110, coupon_code='F15'), # F15: fixed, £15 + dict(id=1, total=200, coupon_code="F20"), # F20: fixed, £20 + dict(id=2, total=150, coupon_code="P30"), # P30: percent, 30% + dict(id=3, total=100, coupon_code="P50"), # P50: percent, 50% + dict(id=4, total=110, coupon_code="F15"), # F15: fixed, £15 ] + for customer in customers: - code = customer['coupon_code'] - if code == 'F20': - customer['discount'] = 20.0 - elif code == 'F15': - customer['discount'] = 15.0 - elif code == 'P30': - customer['discount'] = customer['total'] * 0.3 - elif code == 'P50': - customer['discount'] = customer['total'] * 0.5 + code = customer["coupon_code"] + if code == "F20": + customer["discount"] = 20.0 + elif code == "F15": + customer["discount"] = 15.0 + elif code == "P30": + customer["discount"] = customer["total"] * 0.3 + elif code == "P50": + customer["discount"] = customer["total"] * 0.5 else: - customer['discount'] = 0.0 + customer["discount"] = 0.0 for customer in customers: - print(customer['id'], customer['total'], customer['discount']) + print(customer["id"], customer["total"], customer["discount"]) diff --git a/ch03/discount.py b/ch03/discount.py index 25b2411..871a25c 100644 --- a/ch03/discount.py +++ b/ch03/discount.py @@ -3,15 +3,13 @@ today = date.today() tomorrow = today + timedelta(days=1) # today + 1 day is tomorrow products = [ - {'sku': '1', 'expiration_date': today, 'price': 100.0}, - {'sku': '2', 'expiration_date': tomorrow, 'price': 50}, - {'sku': '3', 'expiration_date': today, 'price': 20}, + {"sku": "1", "expiration_date": today, "price": 100.0}, + {"sku": "2", "expiration_date": tomorrow, "price": 50}, + {"sku": "3", "expiration_date": today, "price": 20}, ] for product in products: - if product['expiration_date'] != today: + if product["expiration_date"] != today: continue - product['price'] *= 0.8 # equivalent to applying 20% discount - print( - 'Price for sku', product['sku'], - 'is now', product['price']) + product["price"] *= 0.8 # equivalent to applying 20% discount + print("Price for sku", product["sku"], "is now", product["price"]) diff --git a/ch03/errorsalert.py b/ch03/errorsalert.py index 80a6358..f64ae5f 100644 --- a/ch03/errorsalert.py +++ b/ch03/errorsalert.py @@ -1,18 +1,19 @@ # The send_email function is defined here to enable you to play with # code, but of course it doesn't send an actual email. def send_email(*a): - print (*a) + print(*a) -alert_system = 'console' # other value can be 'email' -error_severity = 'critical' # other values: 'medium' or 'low' -error_message = 'OMG! Something terrible happened!' -if alert_system == 'console': - print(error_message) #1 -elif alert_system == 'email': - if error_severity == 'critical': - send_email('admin@example.com', error_message) #2 - elif error_severity == 'medium': - send_email('support.1@example.com', error_message) #3 +alert_system = "console" # other value can be 'email' +error_severity = "critical" # other values: 'medium' or 'low' +error_message = "OMG! Something terrible happened!" + +if alert_system == "console": + print(error_message) # 1 +elif alert_system == "email": + if error_severity == "critical": + send_email("admin@example.com", error_message) # 2 + elif error_severity == "medium": + send_email("support.1@example.com", error_message) # 3 else: - send_email('support.2@example.com', error_message) #4 + send_email("support.2@example.com", error_message) # 4 diff --git a/ch03/for.else.py b/ch03/for.else.py index 41f2d01..ba3a378 100644 --- a/ch03/for.else.py +++ b/ch03/for.else.py @@ -2,10 +2,11 @@ class DriverException(Exception): pass -people = [('James', 17), ('Kirk', 9), ('Lars', 13), ('Robert', 8)] +people = [("James", 18), ("Kirk", 9), ("Lars", 13), ("Robert", 8)] for person, age in people: if age >= 18: driver = (person, age) + print(driver) break else: - raise DriverException('Driver not found.') + raise DriverException("Driver not found.") diff --git a/ch03/for.no.else.py b/ch03/for.no.else.py index fd74bac..5d0788e 100644 --- a/ch03/for.no.else.py +++ b/ch03/for.no.else.py @@ -2,12 +2,13 @@ class DriverException(Exception): pass -people = [('James', 17), ('Kirk', 9), ('Lars', 13), ('Robert', 8)] +people = [("James", 17), ("Kirk", 9), ("Lars", 13), ("Robert", 8)] driver = None for person, age in people: if age >= 18: driver = (person, age) + print(driver) break if driver is None: - raise DriverException('Driver not found.') + raise DriverException("Driver not found.") diff --git a/ch03/infinite.py b/ch03/infinite.py index ede3107..f093f1a 100644 --- a/ch03/infinite.py +++ b/ch03/infinite.py @@ -3,6 +3,6 @@ for n in count(5, 3): if n > 20: break - print(n, end=', ') # instead of newline, comma and space + print(n, end=", ") # instead of newline, comma and space print() diff --git a/ch03/multiple.sequences.enumerate.py b/ch03/multiple.sequences.enumerate.py index 621e4ff..30743e0 100644 --- a/ch03/multiple.sequences.enumerate.py +++ b/ch03/multiple.sequences.enumerate.py @@ -1,5 +1,6 @@ -people = ['Nick', 'Rick', 'Roger', 'Syd'] +people = ["Nick", "Rick", "Roger", "Syd"] ages = [23, 24, 23, 21] + for position, person in enumerate(people): age = ages[position] print(person, age) diff --git a/ch03/multiple.sequences.explicit.py b/ch03/multiple.sequences.explicit.py index b8e2aed..ddf4e00 100644 --- a/ch03/multiple.sequences.explicit.py +++ b/ch03/multiple.sequences.explicit.py @@ -1,5 +1,6 @@ -people = ['Nick', 'Rick', 'Roger', 'Syd'] +people = ["Nick", "Rick", "Roger", "Syd"] ages = [23, 24, 23, 21] -instruments = ['Drums', 'Keyboards', 'Bass', 'Guitar'] +instruments = ["Drums", "Keyboards", "Bass", "Guitar"] + for person, age, instrument in zip(people, ages, instruments): print(person, age, instrument) diff --git a/ch03/multiple.sequences.implicit.py b/ch03/multiple.sequences.implicit.py index b1a3c91..b36ac44 100644 --- a/ch03/multiple.sequences.implicit.py +++ b/ch03/multiple.sequences.implicit.py @@ -1,6 +1,7 @@ -people = ['Nick', 'Rick', 'Roger', 'Syd'] +people = ["Nick", "Rick", "Roger", "Syd"] ages = [23, 24, 23, 21] -instruments = ['Drums', 'Keyboards', 'Bass', 'Guitar'] +instruments = ["Drums", "Keyboards", "Bass", "Guitar"] + for data in zip(people, ages, instruments): person, age, instrument = data print(person, age, instrument) diff --git a/ch03/multiple.sequences.py b/ch03/multiple.sequences.py index 6bf71ed..93d313e 100644 --- a/ch03/multiple.sequences.py +++ b/ch03/multiple.sequences.py @@ -1,5 +1,6 @@ -people = ['Nick', 'Rick', 'Roger', 'Syd'] +people = ["Nick", "Rick", "Roger", "Syd"] ages = [23, 24, 23, 21] + for position in range(len(people)): person = people[position] age = ages[position] diff --git a/ch03/multiple.sequences.while.py b/ch03/multiple.sequences.while.py index 723fa9a..cf986b3 100644 --- a/ch03/multiple.sequences.while.py +++ b/ch03/multiple.sequences.while.py @@ -1,6 +1,7 @@ -people = ['Nick', 'Rick', 'Roger', 'Syd'] +people = ["Nick", "Rick", "Roger", "Syd"] ages = [23, 24, 23, 21] position = 0 + while position < len(people): person = people[position] age = ages[position] diff --git a/ch03/multiple.sequences.zip.py b/ch03/multiple.sequences.zip.py index 5b95b95..ecec566 100644 --- a/ch03/multiple.sequences.zip.py +++ b/ch03/multiple.sequences.zip.py @@ -1,4 +1,5 @@ -people = ['Nick', 'Rick', 'Roger', 'Syd'] +people = ["Nick", "Rick", "Roger", "Syd"] ages = [23, 24, 23, 21] + for person, age in zip(people, ages): print(person, age) diff --git a/ch03/permutations.py b/ch03/permutations.py index 95f1af8..c8a8519 100644 --- a/ch03/permutations.py +++ b/ch03/permutations.py @@ -1,2 +1,3 @@ from itertools import permutations -print(list(permutations('ABC'))) + +print(list(permutations("ABCDEF"))) diff --git a/ch03/primes.py b/ch03/primes.py index a0e3f5a..aaa7923 100644 --- a/ch03/primes.py +++ b/ch03/primes.py @@ -1,5 +1,7 @@ primes = [] # this will contain the primes in the end + upto = 100 # the limit, inclusive + for n in range(2, upto + 1): is_prime = True # flag, new at each iteration of outer for for divisor in range(2, n): diff --git a/ch03/range.py b/ch03/range.py index ef5ada2..a668cf7 100644 --- a/ch03/range.py +++ b/ch03/range.py @@ -1,6 +1,11 @@ ->>> list(range(10)) # one value: from 0 to value (excluded) -[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] ->>> list(range(3, 8)) # two values: from start to stop (excluded) -[3, 4, 5, 6, 7] ->>> list(range(-10, 10, 4)) # three values: step is added -[-10, -6, -2, 2, 6] +a = list(range(10)) # one value: from 0 to value (excluded) +print(a) +# [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + +a = list(range(3, 8)) # two values: from start to stop (excluded) +print(a) +# [3, 4, 5, 6, 7] + +a = list(range(-10, 10, 4)) # three values: step is added +print(a) +# [-10, -6, -2, 2, 6] diff --git a/ch03/simple.for.2.py b/ch03/simple.for.2.py index 5882226..9ab6fe0 100644 --- a/ch03/simple.for.2.py +++ b/ch03/simple.for.2.py @@ -1,4 +1,5 @@ -surnames = ['Rivest', 'Shamir', 'Adleman'] +surnames = ["Rivest", "Shamir", "Adleman"] + for position in range(len(surnames)): print(position, surnames[position]) # print(surnames[position][0], end='') # try swapping prints diff --git a/ch03/simple.for.3.py b/ch03/simple.for.3.py index f3333fc..ba61ecf 100644 --- a/ch03/simple.for.3.py +++ b/ch03/simple.for.3.py @@ -1,3 +1,4 @@ -surnames = ['Rivest', 'Shamir', 'Adleman'] +surnames = ["Rivest", "Shamir", "Adleman"] + for surname in surnames: print(surname) diff --git a/ch03/simple.for.4.py b/ch03/simple.for.4.py index 4b9b74a..ded3541 100644 --- a/ch03/simple.for.4.py +++ b/ch03/simple.for.4.py @@ -1,3 +1,4 @@ -surnames = ['Rivest', 'Shamir', 'Adleman'] +surnames = ["Rivest", "Shamir", "Adleman"] + for position, surname in enumerate(surnames): print(position, surname) diff --git a/ch03/switch.py b/ch03/switch.py index 34b49fe..efc38db 100644 --- a/ch03/switch.py +++ b/ch03/switch.py @@ -1,11 +1,12 @@ # this code is not supposed to be run +day_number = 3 + if 1 <= day_number <= 5: - day = 'Weekday' + day = "Weekday" elif day_number == 6: - day = 'Saturday' + day = "Saturday" elif day_number == 0: - day = 'Sunday' + day = "Sunday" else: - day = '' - raise ValueError( - str(day_number) + ' is not a valid day number.') + day = "" + raise ValueError(str(day_number) + " is not a valid day number.") diff --git a/ch03/taxes.py b/ch03/taxes.py index 30d82b6..e98e670 100644 --- a/ch03/taxes.py +++ b/ch03/taxes.py @@ -1,11 +1,12 @@ income = 15000 + if income < 10000: - tax_coefficient = 0.0 #1 + tax_coefficient = 0.0 # 1 elif income < 30000: - tax_coefficient = 0.2 #2 + tax_coefficient = 0.2 # 2 elif income < 100000: - tax_coefficient = 0.35 #3 + tax_coefficient = 0.35 # 3 else: - tax_coefficient = 0.45 #4 + tax_coefficient = 0.45 # 4 -print(f'You will pay: ${income * tax_coefficient} in taxes') +print(f"You will pay: ${income * tax_coefficient} in taxes") diff --git a/ch04/arguments.combined.py b/ch04/arguments.combined.py index c856aa7..5510b85 100644 --- a/ch04/arguments.combined.py +++ b/ch04/arguments.combined.py @@ -2,7 +2,8 @@ def func(a, b, c, d, e, f): print(a, b, c, d, e, f) + func(1, *(2, 3), f=6, *(4, 5)) func(*(1, 2), e=5, *(3, 4), f=6) -func(1, **{'b': 2, 'c': 3}, d=4, **{'e': 5, 'f': 6}) -func(c=3, *(1, 2), **{'d': 4}, e=5, **{'f': 6}) +func(1, **{"b": 2, "c": 3}, d=4, **{"e": 5, "f": 6}) +func(c=3, *(1, 2), **{"d": 4}, e=5, **{"f": 6}) diff --git a/ch04/arguments.keyword.py b/ch04/arguments.keyword.py index 3bc4ea9..c512ed8 100644 --- a/ch04/arguments.keyword.py +++ b/ch04/arguments.keyword.py @@ -2,4 +2,5 @@ def func(a, b, c): print(a, b, c) + func(a=1, c=2, b=3) # prints: 1 3 2 diff --git a/ch04/arguments.multiple.value.py b/ch04/arguments.multiple.value.py index b4cbe17..407f828 100644 --- a/ch04/arguments.multiple.value.py +++ b/ch04/arguments.multiple.value.py @@ -2,6 +2,7 @@ def func(a, b, c): print(a, b, c) + func(2, 3, a=1) """ diff --git a/ch04/arguments.positional.keyword.py b/ch04/arguments.positional.keyword.py index f6b591f..3938fed 100644 --- a/ch04/arguments.positional.keyword.py +++ b/ch04/arguments.positional.keyword.py @@ -2,9 +2,10 @@ def func(a, b, c): print(a, b, c) + func(42, b=1, c=2) -func(b=1, c=2, 42) # positional argument after keyword arguments +# func(b=1, c=2, 42) # positional argument after keyword arguments """ $ python arguments.positional.keyword.py diff --git a/ch04/arguments.positional.py b/ch04/arguments.positional.py index b9ab8b4..e971623 100644 --- a/ch04/arguments.positional.py +++ b/ch04/arguments.positional.py @@ -2,4 +2,5 @@ def func(a, b, c): print(a, b, c) + func(1, 2, 3) # prints: 1 2 3 diff --git a/ch04/arguments.unpack.dict.py b/ch04/arguments.unpack.dict.py index 28f2fec..541ca1b 100644 --- a/ch04/arguments.unpack.dict.py +++ b/ch04/arguments.unpack.dict.py @@ -2,5 +2,6 @@ def func(a, b, c): print(a, b, c) -values = {'b': 1, 'c': 2, 'a': 42} + +values = {"b": 1, "c": 2, "a": 42} func(**values) # equivalent to func(b=1, c=2, a=42) diff --git a/ch04/arguments.unpack.iterable.py b/ch04/arguments.unpack.iterable.py index 31bf7ac..562a2c8 100644 --- a/ch04/arguments.unpack.iterable.py +++ b/ch04/arguments.unpack.iterable.py @@ -2,5 +2,6 @@ def func(a, b, c): print(a, b, c) + values = (1, 3, -7) func(*values) # equivalent to: func(1, 3, -7) diff --git a/ch04/data.science.example.py b/ch04/data.science.example.py index f2bcb69..ad11be7 100644 --- a/ch04/data.science.example.py +++ b/ch04/data.science.example.py @@ -1,4 +1,28 @@ # data.science.example.py +def fetch_data(data_source): + return data_source + + +def parse_data(data): + return data + + +def filter_data(parsed_data): + return parsed_data + + +def polish_data(filtered_data): + return filtered_data + + +def analyse(polished_data): + return polished_data + + +def report(final_data): + return final_data + + def do_report(data_source): # fetch and prepare data data = fetch_data(data_source) @@ -10,13 +34,10 @@ def do_report(data_source): final_data = analyse(polished_data) # create and return report - report = Report(final_data) - return report + report_v = report(final_data) + return report_v if __name__ == "__main__": - - print( - "Please don't call the `do_report` function. " - "It's just and example." - ) + print("Please don't call the `do_report` function. " + "It's just and example.") diff --git a/ch04/docstrings.py b/ch04/docstrings.py index f335062..9396139 100644 --- a/ch04/docstrings.py +++ b/ch04/docstrings.py @@ -1,11 +1,13 @@ # docstrings.py def square(n): - """Return the square of a number n. """ - return n ** 2 + """Return the square of a number n.""" + return n**2 + def get_username(userid): - """Return the username of a user given their id. """ - return db.get(user_id=userid).username + """Return the username of a user given their id.""" + # return db.get(user_id=userid).username + return userid def connect(host, port, user, password): @@ -21,4 +23,5 @@ def connect(host, port, user, password): :return: The connection object. """ # body of the function here... + connection = "" return connection diff --git a/ch04/func.attributes.py b/ch04/func.attributes.py index dfe85f2..633fe6d 100644 --- a/ch04/func.attributes.py +++ b/ch04/func.attributes.py @@ -1,16 +1,23 @@ # func.attributes.py def multiplication(a, b=1): - """Return a multiplied by b. """ + """Return a multiplied by b.""" return a * b if __name__ == "__main__": - special_attributes = [ - "__doc__", "__name__", "__qualname__", "__module__", - "__defaults__", "__code__", "__globals__", "__dict__", - "__closure__", "__annotations__", "__kwdefaults__", + "__doc__", + "__name__", + "__qualname__", + "__module__", + "__defaults__", + "__code__", + "__globals__", + "__dict__", + "__closure__", + "__annotations__", + "__kwdefaults__", ] for attribute in special_attributes: - print(attribute, '->', getattr(multiplication, attribute)) + print(attribute, "->", getattr(multiplication, attribute)) diff --git a/ch04/imports.py b/ch04/imports.py index c69d663..73379c4 100644 --- a/ch04/imports.py +++ b/ch04/imports.py @@ -9,3 +9,6 @@ Exercise, Solution, ) + +print(datetime.date) +print(timezone.tzname) diff --git a/ch04/key.points.argument.passing.py b/ch04/key.points.argument.passing.py index 5a3ca43..1215942 100644 --- a/ch04/key.points.argument.passing.py +++ b/ch04/key.points.argument.passing.py @@ -1,6 +1,9 @@ # key.points.argument.passing.py x = 3 + + def func(y): print(y) + func(x) # prints: 3 diff --git a/ch04/key.points.assignment.py b/ch04/key.points.assignment.py index 440f577..e237207 100644 --- a/ch04/key.points.assignment.py +++ b/ch04/key.points.assignment.py @@ -1,7 +1,11 @@ # key.points.assignment.py x = 3 + + def func(x): x = 7 # defining a local x, not changing the global one + print(x) + func(x) print(x) # prints: 3 diff --git a/ch04/key.points.mutable.assignment.py b/ch04/key.points.mutable.assignment.py index 458ca86..07457e2 100644 --- a/ch04/key.points.mutable.assignment.py +++ b/ch04/key.points.mutable.assignment.py @@ -1,8 +1,11 @@ # key.points.mutable.assignment.py x = [1, 2, 3] + + def func(x): x[1] = 42 # this changes the original `x` argument! - x = 'something else' # this points x to a new string object + x = "something else" # this points x to a new string object + func(x) print(x) # still prints: [1, 42, 3] diff --git a/ch04/key.points.mutable.py b/ch04/key.points.mutable.py index 16ac26d..709693f 100644 --- a/ch04/key.points.mutable.py +++ b/ch04/key.points.mutable.py @@ -1,7 +1,10 @@ # key.points.mutable.py x = [1, 2, 3] + + def func(x): x[1] = 42 # this affects the `x` argument! + func(x) print(x) # prints: [1, 42, 3] diff --git a/ch04/lambda.explained.py b/ch04/lambda.explained.py index cbed5e5..422e747 100644 --- a/ch04/lambda.explained.py +++ b/ch04/lambda.explained.py @@ -6,23 +6,26 @@ def myfunc([parameter_list]): return expression """ + # example 1: adder def adder(a, b): return a + b + # is equivalent to: adder_lambda = lambda a, b: a + b + # example 2: to uppercase def to_upper(s): return s.upper() + # is equivalent to: to_upper_lambda = lambda s: s.upper() if __name__ == "__main__": - print(adder(3, 4)) print(adder_lambda(3, 4)) diff --git a/ch04/lib/funcdef.py b/ch04/lib/funcdef.py index 6130290..1c9acfd 100644 --- a/ch04/lib/funcdef.py +++ b/ch04/lib/funcdef.py @@ -1,6 +1,7 @@ # lib/funcdef.py def square(n): - return n ** 2 + return n**2 + def cube(n): - return n ** 3 + return n**3 diff --git a/ch04/matrix.multiplication.func.py b/ch04/matrix.multiplication.func.py index 6dca351..6dc5227 100644 --- a/ch04/matrix.multiplication.func.py +++ b/ch04/matrix.multiplication.func.py @@ -1,8 +1,7 @@ # matrix.multiplication.func.py # this function could also be defined in another module def matrix_mul(a, b): - return [[sum(i * j for i, j in zip(r, c)) for c in zip(*b)] - for r in a] + return [[sum(i * j for i, j in zip(r, c)) for c in zip(*b)] for r in a] a = [[1, 2], [3, 4]] diff --git a/ch04/matrix.multiplication.nofunc.py b/ch04/matrix.multiplication.nofunc.py index f023bba..d95fde8 100644 --- a/ch04/matrix.multiplication.nofunc.py +++ b/ch04/matrix.multiplication.nofunc.py @@ -2,6 +2,5 @@ a = [[1, 2], [3, 4]] b = [[5, 1], [2, 1]] -c = [[sum(i * j for i, j in zip(r, c)) for c in zip(*b)] - for r in a] +c = [[sum(i * j for i, j in zip(r, c)) for c in zip(*b)] for r in a] print(c) diff --git a/ch04/no.side.effects.py b/ch04/no.side.effects.py index eee7a02..fe9def7 100644 --- a/ch04/no.side.effects.py +++ b/ch04/no.side.effects.py @@ -1,10 +1,11 @@ # no.side.effects.py -# This file is not meant to be run ->>> numbers = [4, 1, 7, 5] ->>> sorted(numbers) # won't sort the original `numbers` list -[1, 4, 5, 7] ->>> numbers # let's verify -[4, 1, 7, 5] # good, untouched ->>> numbers.sort() # this will act on the list ->>> numbers -[1, 4, 5, 7] + +numbers = [4, 1, 7, 5] +print(sorted(numbers)) # won't sort the original `numbers` list + +print(numbers) # let's verify +# [4, 1, 7, 5] # good, untouched + +numbers.sort() # this will act on the list +print(numbers) +# [1, 4, 5, 7] diff --git a/ch04/parameters.all.pkwonly.py b/ch04/parameters.all.pkwonly.py index c87624e..60ba300 100644 --- a/ch04/parameters.all.pkwonly.py +++ b/ch04/parameters.all.pkwonly.py @@ -1,8 +1,9 @@ # parameters.all.pkwonly.py -def allparams(a, /, b, c=42, *args, d=256, e, **kwargs): - print('a, b, c:', a, b, c) - print('d, e:', d, e) - print('args:', args) - print('kwargs:', kwargs) +def allparams(a, /, b, c=42, *args, d=256, e, **kwargs): + print("a, b, c:", a, b, c) + print("d, e:", d, e) + print("args:", args) + print("kwargs:", kwargs) + allparams(1, 2, 3, 4, 5, 6, e=7, f=9, g=10) diff --git a/ch04/parameters.all.py b/ch04/parameters.all.py index cbad779..9944db4 100644 --- a/ch04/parameters.all.py +++ b/ch04/parameters.all.py @@ -1,7 +1,8 @@ # parameters.all.py def func(a, b, c=7, *args, **kwargs): - print('a, b, c:', a, b, c) - print('args:', args) - print('kwargs:', kwargs) + print("a, b, c:", a, b, c) + print("args:", args) + print("kwargs:", kwargs) -func(1, 2, 3, 5, 7, 9, A='a', B='b') + +func(1, 2, 3, 5, 7, 9, A="a", B="b") diff --git a/ch04/parameters.default.py b/ch04/parameters.default.py index 1e9672e..a444f45 100644 --- a/ch04/parameters.default.py +++ b/ch04/parameters.default.py @@ -2,7 +2,8 @@ def func(a, b=4, c=88): print(a, b, c) -func(1) # prints: 1 4 88 + +func(1) # prints: 1 4 88 func(b=5, a=7, c=9) # prints: 7 5 9 -func(42, c=9) # prints: 42 4 9 -func(42, 43, 44) # prints: 42, 43, 44 +func(42, c=9) # prints: 42 4 9 +func(42, 43, 44) # prints: 42, 43, 44 diff --git a/ch04/parameters.defaults.mutable.intermediate.call.py b/ch04/parameters.defaults.mutable.intermediate.call.py index ac216c2..1ed27a3 100644 --- a/ch04/parameters.defaults.mutable.intermediate.call.py +++ b/ch04/parameters.defaults.mutable.intermediate.call.py @@ -2,10 +2,11 @@ def func(a=[], b={}): print(a) print(b) - print('#' * 12) + print("#" * 12) a.append(len(a)) # this will affect a's default value b[len(a)] = len(a) # and this will affect b's one + func() -func(a=[1, 2, 3], b={'B': 1}) +func(a=[1, 2, 3], b={"B": 1}) func() diff --git a/ch04/parameters.defaults.mutable.py b/ch04/parameters.defaults.mutable.py index 59ee6ce..989aaf8 100644 --- a/ch04/parameters.defaults.mutable.py +++ b/ch04/parameters.defaults.mutable.py @@ -2,10 +2,11 @@ def func(a=[], b={}): print(a) print(b) - print('#' * 12) + print("#" * 12) a.append(len(a)) # this will affect a's default value b[len(a)] = len(a) # and this will affect b's one + func() func() func() diff --git a/ch04/parameters.keyword.only.py b/ch04/parameters.keyword.only.py index b33b2b2..0e9c21b 100644 --- a/ch04/parameters.keyword.only.py +++ b/ch04/parameters.keyword.only.py @@ -2,15 +2,18 @@ def kwo(*a, c): print(a, c) + kwo(1, 2, 3, c=7) # prints: (1, 2, 3) 7 -kwo(c=4) # prints: () 4 +kwo(c=4) # prints: () 4 # kwo(1, 2) # breaks, invalid syntax, with the following error # TypeError: kwo() missing 1 required keyword-only argument: 'c' + def kwo2(a, b=42, *, c): print(a, b, c) + kwo2(3, b=7, c=99) # prints: 3 7 99 -kwo2(3, c=13) # prints: 3 42 13 +kwo2(3, c=13) # prints: 3 42 13 # kwo2(3, 23) # breaks, invalid syntax, with the following error # TypeError: kwo2() missing 1 required keyword-only argument: 'c' diff --git a/ch04/parameters.positional.only.optional.py b/ch04/parameters.positional.only.optional.py index 1c5ba36..a8078b8 100644 --- a/ch04/parameters.positional.only.optional.py +++ b/ch04/parameters.positional.only.optional.py @@ -2,5 +2,6 @@ def func(a, b=2, /): print(a, b) + func(4, 5) # prints 4 5 func(3) # prints 3 2 diff --git a/ch04/parameters.positional.only.py b/ch04/parameters.positional.only.py index 828ff40..8dc0797 100644 --- a/ch04/parameters.positional.only.py +++ b/ch04/parameters.positional.only.py @@ -2,9 +2,11 @@ def func(a, b, /, c): print(a, b, c) + func(1, 2, 3) # prints: 1 2 3 func(1, 2, c=3) # prints 1 2 3 # func(1, b=2, c=3) # produces the following traceback: + """ Traceback (most recent call last): File "arguments.positional.only.py", line 7, in @@ -46,11 +48,13 @@ def divmod(a, b, /): len(obj='hello') # The "obj" keyword argument impairs readability """ + def func_name(name, /, **kwargs): print(name) print(kwargs) -func_name('Positional-only name', name='Name in **kwargs') + +func_name("Positional-only name", name="Name in **kwargs") """ Prints: Positional-only name diff --git a/ch04/parameters.variable.db.py b/ch04/parameters.variable.db.py index c8205e2..9571354 100644 --- a/ch04/parameters.variable.db.py +++ b/ch04/parameters.variable.db.py @@ -1,15 +1,16 @@ # parameters.variable.db.py def connect(**options): conn_params = { - 'host': options.get('host', '127.0.0.1'), - 'port': options.get('port', 5432), - 'user': options.get('user', ''), - 'pwd': options.get('pwd', ''), + "host": options.get("host", "127.0.0.1"), + "port": options.get("port", 5432), + "user": options.get("user", ""), + "pwd": options.get("pwd", ""), } print(conn_params) # we then connect to the db (commented out) # db.connect(**conn_params) + connect() -connect(host='127.0.0.42', port=5433) -connect(port=5431, user='fab', pwd='gandalf') +connect(host="127.0.0.42", port=5433) +connect(port=5431, user="fab", pwd="gandalf") diff --git a/ch04/parameters.variable.keyword.py b/ch04/parameters.variable.keyword.py index 9240a1c..79a9382 100644 --- a/ch04/parameters.variable.keyword.py +++ b/ch04/parameters.variable.keyword.py @@ -2,6 +2,7 @@ def func(**kwargs): print(kwargs) + func(a=1, b=42) # prints {'a': 1, 'b': 42} func() # prints {} func(a=1, b=46, c=99) # prints {'a': 1, 'b': 46, 'c': 99} diff --git a/ch04/parameters.variable.positional.py b/ch04/parameters.variable.positional.py index a52d4e5..5315545 100644 --- a/ch04/parameters.variable.positional.py +++ b/ch04/parameters.variable.positional.py @@ -8,5 +8,6 @@ def minimum(*n): mn = value print(mn) + minimum(1, 3, -7, 9) # n = (1, 3, -7, 9) - prints: -7 -minimum() # n = () - prints: nothing +minimum() # n = () - prints: nothing diff --git a/ch04/primes.py b/ch04/primes.py index 27f1c00..5700163 100644 --- a/ch04/primes.py +++ b/ch04/primes.py @@ -3,7 +3,7 @@ def get_primes(n): - """Calculate a list of primes up to n (included). """ + """Calculate a list of primes up to n (included).""" primelist = [] for candidate in range(2, n + 1): is_prime = True @@ -24,21 +24,174 @@ def get_primes(n): def test(): primes = get_primes(10**3) primes2 = [ - 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, - 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, - 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, - 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, - 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, - 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, - 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, - 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, - 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, - 571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, - 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, - 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, - 787, 797, 809, 811, 821, 823, 827, 829, 839, 853, 857, - 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, - 941, 947, 953, 967, 971, 977, 983, 991, 997 + 2, + 3, + 5, + 7, + 11, + 13, + 17, + 19, + 23, + 29, + 31, + 37, + 41, + 43, + 47, + 53, + 59, + 61, + 67, + 71, + 73, + 79, + 83, + 89, + 97, + 101, + 103, + 107, + 109, + 113, + 127, + 131, + 137, + 139, + 149, + 151, + 157, + 163, + 167, + 173, + 179, + 181, + 191, + 193, + 197, + 199, + 211, + 223, + 227, + 229, + 233, + 239, + 241, + 251, + 257, + 263, + 269, + 271, + 277, + 281, + 283, + 293, + 307, + 311, + 313, + 317, + 331, + 337, + 347, + 349, + 353, + 359, + 367, + 373, + 379, + 383, + 389, + 397, + 401, + 409, + 419, + 421, + 431, + 433, + 439, + 443, + 449, + 457, + 461, + 463, + 467, + 479, + 487, + 491, + 499, + 503, + 509, + 521, + 523, + 541, + 547, + 557, + 563, + 569, + 571, + 577, + 587, + 593, + 599, + 601, + 607, + 613, + 617, + 619, + 631, + 641, + 643, + 647, + 653, + 659, + 661, + 673, + 677, + 683, + 691, + 701, + 709, + 719, + 727, + 733, + 739, + 743, + 751, + 757, + 761, + 769, + 773, + 787, + 797, + 809, + 811, + 821, + 823, + 827, + 829, + 839, + 853, + 857, + 859, + 863, + 877, + 881, + 883, + 887, + 907, + 911, + 919, + 929, + 937, + 941, + 947, + 953, + 967, + 971, + 977, + 983, + 991, + 997, ] return primes == primes2 diff --git a/ch04/return.multiple.py b/ch04/return.multiple.py index 00fac92..49fdc72 100644 --- a/ch04/return.multiple.py +++ b/ch04/return.multiple.py @@ -5,7 +5,8 @@ def moddiv(a, b): def test(n=10**4): from random import choice, random, randint - m = 10 ** 6 + + m = 10**6 for x in range(n): a = choice((randint(0, m), m * random())) b = 0 @@ -15,12 +16,11 @@ def test(n=10**4): r = divmod(a, b) r2 = moddiv(a, b) if r != r2: - print('Difference: ', a, b, r, r2) + print("Difference: ", a, b, r, r2) if __name__ == "__main__": - - test(10 ** 6) - print('Done') + test(10**6) + print("Done") print(moddiv(20, 7)) # prints (2, 6) diff --git a/ch04/return.none.py b/ch04/return.none.py index 9e246f8..8869e54 100644 --- a/ch04/return.none.py +++ b/ch04/return.none.py @@ -2,6 +2,7 @@ def func(): pass + func() # the return of this call won't be collected. It's lost. a = func() # the return of this one instead is collected into `a` print(a) # prints: None diff --git a/ch04/return.single.value.py b/ch04/return.single.value.py index dad0099..6e5fd13 100644 --- a/ch04/return.single.value.py +++ b/ch04/return.single.value.py @@ -7,5 +7,6 @@ def factorial(n): result *= k return result + f5 = factorial(5) # f5 = 120 print(f5) diff --git a/ch04/scoping.level.1.py b/ch04/scoping.level.1.py index b40a57a..bf36500 100644 --- a/ch04/scoping.level.1.py +++ b/ch04/scoping.level.1.py @@ -1,9 +1,9 @@ # scoping.level.1.py def my_function(): - test = 1 # this is defined in the local scope of the function - print('my_function:', test) + test = 1 # this is defined in the local scope of the function + print("my_function:", test) test = 0 # this is defined in the global scope my_function() -print('global:', test) +print("global:", test) diff --git a/ch04/scoping.level.2.global.py b/ch04/scoping.level.2.global.py index 67349e1..298a6d5 100644 --- a/ch04/scoping.level.2.global.py +++ b/ch04/scoping.level.2.global.py @@ -5,12 +5,12 @@ def outer(): def inner(): global test test = 2 # global scope - print('inner:', test) + print("inner:", test) inner() - print('outer:', test) + print("outer:", test) test = 0 # global scope outer() -print('global:', test) +print("global:", test) diff --git a/ch04/scoping.level.2.nonlocal.py b/ch04/scoping.level.2.nonlocal.py index 1e1c574..62c2707 100644 --- a/ch04/scoping.level.2.nonlocal.py +++ b/ch04/scoping.level.2.nonlocal.py @@ -5,12 +5,12 @@ def outer(): def inner(): nonlocal test test = 2 # nearest enclosing scope (which is 'outer') - print('inner:', test) + print("inner:", test) inner() - print('outer:', test) + print("outer:", test) test = 0 # global scope outer() -print('global:', test) +print("global:", test) diff --git a/ch04/scoping.level.2.py b/ch04/scoping.level.2.py index ac2dece..919e322 100644 --- a/ch04/scoping.level.2.py +++ b/ch04/scoping.level.2.py @@ -4,12 +4,12 @@ def outer(): def inner(): test = 2 # inner scope - print('inner:', test) + print("inner:", test) inner() - print('outer:', test) + print("outer:", test) test = 0 # global scope outer() -print('global:', test) +print("global:", test) diff --git a/ch04/vat.py b/ch04/vat.py index cb0a1d3..e30ebb1 100644 --- a/ch04/vat.py +++ b/ch04/vat.py @@ -8,5 +8,5 @@ if __name__ == "__main__": for var, value in sorted(vars().items()): - if 'price' in var: + if "price" in var: print(var, value) diff --git a/ch05/decorate.sort.undecorate.py b/ch05/decorate.sort.undecorate.py index b0e90dc..638fdd5 100644 --- a/ch05/decorate.sort.undecorate.py +++ b/ch05/decorate.sort.undecorate.py @@ -12,7 +12,9 @@ def decorate(student): # create a 2-tuple (sum of credits, student) from student dict - return (sum(student['credits'].values()), student) + return (sum(student["credits"].values()), student) + + pprint(decorate(students[0])) diff --git a/ch05/dictionary.comprehensions.duplicates.py b/ch05/dictionary.comprehensions.duplicates.py index bc29418..d995896 100644 --- a/ch05/dictionary.comprehensions.duplicates.py +++ b/ch05/dictionary.comprehensions.duplicates.py @@ -1,5 +1,5 @@ # dictionary.comprehensions.duplicates.py -word = 'Hello' +word = "Hello" swaps = {c: c.swapcase() for c in word} diff --git a/ch05/dictionary.comprehensions.positions.py b/ch05/dictionary.comprehensions.positions.py index 050ad70..85ee3b9 100644 --- a/ch05/dictionary.comprehensions.positions.py +++ b/ch05/dictionary.comprehensions.positions.py @@ -1,5 +1,5 @@ # dictionary.comprehensions.positions.py -word = 'Hello' +word = "Hello" positions = {c: k for k, c in enumerate(word)} diff --git a/ch05/dictionary.comprehensions.py b/ch05/dictionary.comprehensions.py index bd95924..b0ab790 100644 --- a/ch05/dictionary.comprehensions.py +++ b/ch05/dictionary.comprehensions.py @@ -1,10 +1,10 @@ # dictionary.comprehensions.py from string import ascii_lowercase +from pprint import pprint lettermap = {c: k for k, c in enumerate(ascii_lowercase, 1)} # lettermap = dict((c, k) for k, c in enumerate(ascii_lowercase, 1)) -from pprint import pprint pprint(lettermap) diff --git a/ch05/even.squares.py b/ch05/even.squares.py index 5182d5f..2a73ec7 100644 --- a/ch05/even.squares.py +++ b/ch05/even.squares.py @@ -1,10 +1,8 @@ # even.squares.py # using map and filter -sq1 = list( - map(lambda n: n ** 2, filter(lambda n: not n % 2, range(10))) -) +sq1 = list(map(lambda n: n**2, filter(lambda n: not n % 2, range(10)))) # equivalent, but using list comprehensions -sq2 = [n ** 2 for n in range(10) if not n % 2] +sq2 = [n**2 for n in range(10) if not n % 2] print(sq1, sq1 == sq2) # prints: [0, 4, 16, 36, 64] True diff --git a/ch05/fibonacci.elegant.py b/ch05/fibonacci.elegant.py index 52d7c90..688ddea 100644 --- a/ch05/fibonacci.elegant.py +++ b/ch05/fibonacci.elegant.py @@ -1,12 +1,12 @@ # fibonacci.elegant.py def fibonacci(N): - """Return all fibonacci numbers up to N. """ + """Return all fibonacci numbers up to N.""" a, b = 0, 1 while a <= N: yield a a, b = b, a + b -print(list(fibonacci(0))) # [0] -print(list(fibonacci(1))) # [0, 1, 1] +print(list(fibonacci(0))) # [0] +print(list(fibonacci(1))) # [0, 1, 1] print(list(fibonacci(50))) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] diff --git a/ch05/fibonacci.first.py b/ch05/fibonacci.first.py index fdcfa6f..4350679 100644 --- a/ch05/fibonacci.first.py +++ b/ch05/fibonacci.first.py @@ -1,6 +1,6 @@ # fibonacci.first.py def fibonacci(N): - """Return all fibonacci numbers up to N. """ + """Return all fibonacci numbers up to N.""" result = [0] next_n = 1 while next_n <= N: @@ -9,6 +9,6 @@ def fibonacci(N): return result -print(fibonacci(0)) # [0] -print(fibonacci(1)) # [0, 1, 1] +print(fibonacci(0)) # [0] +print(fibonacci(1)) # [0, 1, 1] print(fibonacci(50)) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] diff --git a/ch05/fibonacci.second.py b/ch05/fibonacci.second.py index 033bca3..3e01d57 100644 --- a/ch05/fibonacci.second.py +++ b/ch05/fibonacci.second.py @@ -1,6 +1,6 @@ # fibonacci.second.py def fibonacci(N): - """Return all fibonacci numbers up to N. """ + """Return all fibonacci numbers up to N.""" yield 0 if N == 0: return @@ -11,6 +11,6 @@ def fibonacci(N): a, b = b, a + b -print(list(fibonacci(0))) # [0] -print(list(fibonacci(1))) # [0, 1, 1] +print(list(fibonacci(0))) # [0] +print(list(fibonacci(1))) # [0, 1, 1] print(list(fibonacci(50))) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] diff --git a/ch05/filter.py b/ch05/filter.py index 3e5495f..5490751 100644 --- a/ch05/filter.py +++ b/ch05/filter.py @@ -1,10 +1,6 @@ # filter.py -# This is not a valid Python module - Don't run it. ->>> test = [2, 5, 8, 0, 0, 1, 0] ->>> list(filter(None, test)) -[2, 5, 8, 1] ->>> list(filter(lambda x: x, test)) # equivalent to previous one -[2, 5, 8, 1] ->>> list(filter(lambda x: x > 4, test)) # keep only items > 4 -[5, 8] +test = list((2, 5, 8, 0, 0, 1, 0)) +print(list(filter(None, test))) +print(list(filter(lambda x: x, test))) # equivalent to previous one +print(list(filter(lambda x: x > 4, test))) # keep only items > 4 diff --git a/ch05/first.n.squares.manual.method.py b/ch05/first.n.squares.manual.method.py index 29c68c1..1411fe5 100644 --- a/ch05/first.n.squares.manual.method.py +++ b/ch05/first.n.squares.manual.method.py @@ -1,7 +1,8 @@ # first.n.squares.manual.method.py def get_squares_gen(n): for x in range(n): - yield x ** 2 + yield x**2 + squares = get_squares_gen(3) print(squares.__next__()) # prints: 0 @@ -9,4 +10,4 @@ def get_squares_gen(n): print(squares.__next__()) # prints: 4 # the following raises StopIteration, the generator is exhausted, # any further call to next will keep raising StopIteration -print(squares.__next__()) +# print(squares.__next__()) diff --git a/ch05/first.n.squares.manual.py b/ch05/first.n.squares.manual.py index 4d28174..e120609 100644 --- a/ch05/first.n.squares.manual.py +++ b/ch05/first.n.squares.manual.py @@ -1,7 +1,8 @@ # first.n.squares.manual.py def get_squares_gen(n): for x in range(n): - yield x ** 2 + yield x**2 + squares = get_squares_gen(4) # this creates a generator object print(squares) # @@ -11,4 +12,4 @@ def get_squares_gen(n): print(next(squares)) # prints: 9 # the following raises StopIteration, the generator is exhausted, # any further call to next will keep raising StopIteration -print(next(squares)) +# print(next(squares)) diff --git a/ch05/first.n.squares.py b/ch05/first.n.squares.py index b5bca28..f7c6937 100644 --- a/ch05/first.n.squares.py +++ b/ch05/first.n.squares.py @@ -1,12 +1,14 @@ # first.n.squares.py def get_squares(n): # classic function approach - return [x ** 2 for x in range(n)] + return [x**2 for x in range(n)] + print(get_squares(10)) def get_squares_gen(n): # generator approach for x in range(n): - yield x ** 2 # we yield, we don't return + yield x**2 # we yield, we don't return + print(list(get_squares_gen(10))) diff --git a/ch05/functions.py b/ch05/functions.py index 60b5b3d..3143dba 100644 --- a/ch05/functions.py +++ b/ch05/functions.py @@ -1,7 +1,8 @@ # functions.py + def gcd(a, b): - """Calculate the Greatest Common Divisor of (a, b). """ + """Calculate the Greatest Common Divisor of (a, b).""" while b != 0: a, b = b, a % b return a diff --git a/ch05/gen.map.filter.py b/ch05/gen.map.filter.py index c7d4e69..85f8a72 100644 --- a/ch05/gen.map.filter.py +++ b/ch05/gen.map.filter.py @@ -3,12 +3,10 @@ N = 20 cubes1 = map( - lambda n: (n, n**3), - filter(lambda n: n % 3 == 0 or n % 5 == 0, range(N)) + lambda n: (n, n**3), filter(lambda n: n % 3 == 0 or n % 5 == 0, range(N)) ) -cubes2 = ( - (n, n**3) for n in range(N) if n % 3 == 0 or n % 5 == 0) +cubes2 = ((n, n**3) for n in range(N) if n % 3 == 0 or n % 5 == 0) print(list(cubes1)) print(list(cubes2)) diff --git a/ch05/gen.map.py b/ch05/gen.map.py index 0735688..e71d5b1 100644 --- a/ch05/gen.map.py +++ b/ch05/gen.map.py @@ -2,5 +2,6 @@ def adder(*n): return sum(n) + s1 = sum(map(adder, range(100), range(1, 101))) s2 = sum(adder(*n) for n in zip(range(100), range(1, 101))) diff --git a/ch05/gen.send.preparation.py b/ch05/gen.send.preparation.py index 642ab58..c62d2fb 100644 --- a/ch05/gen.send.preparation.py +++ b/ch05/gen.send.preparation.py @@ -5,6 +5,7 @@ def counter(start=0): yield n n += 1 + c = counter() print(next(c)) # prints: 0 print(next(c)) # prints: 1 diff --git a/ch05/gen.send.preparation.stop.py b/ch05/gen.send.preparation.stop.py index 8c5b192..e4ad0ed 100644 --- a/ch05/gen.send.preparation.stop.py +++ b/ch05/gen.send.preparation.stop.py @@ -1,14 +1,16 @@ # gen.send.preparation.stop.py stop = False + def counter(start=0): n = start while not stop: yield n n += 1 + c = counter() print(next(c)) # prints: 0 print(next(c)) # prints: 1 stop = True -print(next(c)) # raises StopIteration +# print(next(c)) # raises StopIteration diff --git a/ch05/gen.send.py b/ch05/gen.send.py index efbd393..cdf0f41 100644 --- a/ch05/gen.send.py +++ b/ch05/gen.send.py @@ -2,14 +2,15 @@ def counter(start=0): n = start while True: - result = yield n # A + result = yield n # A print(type(result), result) # B - if result == 'Q': + if result == "Q": break n += 1 + c = counter() -print(next(c)) # C -print(c.send('Wow!')) # D -print(next(c)) # E -print(c.send('Q')) # F +print(next(c)) # C +print(c.send("Wow!")) # D +print(next(c)) # E +# print(c.send('Q')) # F diff --git a/ch05/gen.yield.for.py b/ch05/gen.yield.for.py index 1826b95..81a9951 100644 --- a/ch05/gen.yield.for.py +++ b/ch05/gen.yield.for.py @@ -1,7 +1,8 @@ # gen.yield.for.py def print_squares(start, end): for n in range(start, end): - yield n ** 2 + yield n**2 + for n in print_squares(2, 5): print(n) diff --git a/ch05/gen.yield.from.py b/ch05/gen.yield.from.py index f09a23e..8dcca18 100644 --- a/ch05/gen.yield.from.py +++ b/ch05/gen.yield.from.py @@ -1,6 +1,7 @@ # gen.yield.from.py def print_squares(start, end): - yield from (n ** 2 for n in range(start, end)) + yield from (n**2 for n in range(start, end)) + for n in print_squares(2, 5): print(n) diff --git a/ch05/gen.yield.return.py b/ch05/gen.yield.return.py index 3ffdcab..9751613 100644 --- a/ch05/gen.yield.return.py +++ b/ch05/gen.yield.return.py @@ -1,5 +1,6 @@ # gen.yield.return.py + def geometric_progression(a, q): k = 0 while True: diff --git a/ch05/generator.expressions.py b/ch05/generator.expressions.py index 908ba98..8e3c9d2 100644 --- a/ch05/generator.expressions.py +++ b/ch05/generator.expressions.py @@ -1,17 +1,17 @@ # generator.expressions.py # This is not a valid Python module - Don't run it. ->>> cubes = [k**3 for k in range(10)] # regular list ->>> cubes -[0, 1, 8, 27, 64, 125, 216, 343, 512, 729] ->>> type(cubes) - ->>> cubes_gen = (k**3 for k in range(10)) # create as generator ->>> cubes_gen - at 0x103fb5a98> ->>> type(cubes_gen) - ->>> list(cubes_gen) # this will exhaust the generator -[0, 1, 8, 27, 64, 125, 216, 343, 512, 729] ->>> list(cubes_gen) # nothing more to give -[] + +cubes = [k**3 for k in range(10)] # regular list +print(cubes) +print(type(cubes)) + +cubes_gen = (k**3 for k in range(10)) # create as generator +print(cubes_gen) +print(type(cubes_gen)) + +# +# >>> list(cubes_gen) # this will exhaust the generator +# [0, 1, 8, 27, 64, 125, 216, 343, 512, 729] +# >>> list(cubes_gen) # nothing more to give +# [] diff --git a/ch05/list.iterable.py b/ch05/list.iterable.py index 2fe129d..32850cf 100644 --- a/ch05/list.iterable.py +++ b/ch05/list.iterable.py @@ -1,7 +1,7 @@ # list.iterable.py # this code won't run ->>> range(7) -range(0, 7) ->>> list(range(7)) # put all elements in a list to view them -[0, 1, 2, 3, 4, 5, 6] +# >>> range(7) +# range(0, 7) +# >>> list(range(7)) # put all elements in a list to view them +# [0, 1, 2, 3, 4, 5, 6] diff --git a/ch05/map.example.py b/ch05/map.example.py index 67b23ff..7b8c927 100644 --- a/ch05/map.example.py +++ b/ch05/map.example.py @@ -1,18 +1,18 @@ # map.example.py # This is not a valid Python module - Don't run it. ->>> map(lambda *a: a, range(3)) # 1 iterable - # Not useful! Let's use list ->>> list(map(lambda *a: a, range(3))) # 1 iterable -[(0,), (1,), (2,)] ->>> list(map(lambda *a: a, range(3), 'abc')) # 2 iterables -[(0, 'a'), (1, 'b'), (2, 'c')] ->>> list(map(lambda *a: a, range(3), 'abc', range(4, 7))) # 3 -[(0, 'a', 4), (1, 'b', 5), (2, 'c', 6)] ->>> # map stops at the shortest iterator ->>> list(map(lambda *a: a, (), 'abc')) # empty tuple is shortest -[] ->>> list(map(lambda *a: a, (1, 2), 'abc')) # (1, 2) shortest -[(1, 'a'), (2, 'b')] ->>> list(map(lambda *a: a, (1, 2, 3, 4), 'abc')) # 'abc' shortest -[(1, 'a'), (2, 'b'), (3, 'c')] +# >>> map(lambda *a: a, range(3)) # 1 iterable +# # Not useful! Let's use list +# >>> list(map(lambda *a: a, range(3))) # 1 iterable +# [(0,), (1,), (2,)] +# >>> list(map(lambda *a: a, range(3), 'abc')) # 2 iterables +# [(0, 'a'), (1, 'b'), (2, 'c')] +# >>> list(map(lambda *a: a, range(3), 'abc', range(4, 7))) # 3 +# [(0, 'a', 4), (1, 'b', 5), (2, 'c', 6)] +# >>> # map stops at the shortest iterator +# >>> list(map(lambda *a: a, (), 'abc')) # empty tuple is shortest +# [] +# >>> list(map(lambda *a: a, (1, 2), 'abc')) # (1, 2) shortest +# [(1, 'a'), (2, 'b')] +# >>> list(map(lambda *a: a, (1, 2, 3, 4), 'abc')) # 'abc' shortest +# [(1, 'a'), (2, 'b'), (3, 'c')] diff --git a/ch05/maxims.py b/ch05/maxims.py index 5cf3686..a1cc6e1 100644 --- a/ch05/maxims.py +++ b/ch05/maxims.py @@ -1,9 +1,8 @@ # maxims.py -# This is not a valid Python module - Don't run it. ->>> a = [5, 9, 2, 4, 7] ->>> b = [3, 7, 1, 9, 2] ->>> c = [6, 8, 0, 5, 3] ->>> maxs = map(lambda n: max(*n), zip(a, b, c)) ->>> list(maxs) -[6, 9, 2, 9, 7] +a = [5, 9, 2, 4, 7] +b = [3, 7, 1, 9, 2] +c = [6, 8, 0, 5, 3] +maxs = map(lambda n: max(*n), zip(a, b, c)) +print(list(maxs)) +# [6, 9, 2, 9, 7] diff --git a/ch05/pairs.for.loop.py b/ch05/pairs.for.loop.py index b3de560..340ee0b 100644 --- a/ch05/pairs.for.loop.py +++ b/ch05/pairs.for.loop.py @@ -1,6 +1,6 @@ # pairs.for.loop.py -items = 'ABCD' +items = "ABCD" pairs = [] for a in range(len(items)): diff --git a/ch05/pairs.list.comprehension.py b/ch05/pairs.list.comprehension.py index 5a95f28..b90a3e3 100644 --- a/ch05/pairs.list.comprehension.py +++ b/ch05/pairs.list.comprehension.py @@ -1,7 +1,6 @@ # pairs.list.comprehension.py -items = 'ABCD' -pairs = [(items[a], items[b]) - for a in range(len(items)) for b in range(a, len(items))] +items = "ABCD" +pairs = [(items[a], items[b]) for a in range(len(items)) for b in range(a, len(items))] print(pairs) diff --git a/ch05/performance.map.py b/ch05/performance.map.py index 129ede9..d3b839c 100644 --- a/ch05/performance.map.py +++ b/ch05/performance.map.py @@ -1,20 +1,20 @@ # performance.map.py from time import time -mx = 2 * 10 ** 7 +mx = 2 * 10**7 t = time() absloop = [] for n in range(mx): absloop.append(abs(n)) -print('for loop: {:.4f} s'.format(time() - t)) +print("for loop: {:.4f} s".format(time() - t)) t = time() abslist = [abs(n) for n in range(mx)] -print('list comprehension: {:.4f} s'.format(time() - t)) +print("list comprehension: {:.4f} s".format(time() - t)) t = time() absmap = list(map(abs, range(mx))) -print('map: {:.4f} s'.format(time() - t)) +print("map: {:.4f} s".format(time() - t)) print(absloop == abslist == absmap) diff --git a/ch05/performance.py b/ch05/performance.py index b9dff15..5b2c2bb 100644 --- a/ch05/performance.py +++ b/ch05/performance.py @@ -8,22 +8,18 @@ for a in range(1, mx): for b in range(a, mx): floop.append(divmod(a, b)) -print('for loop: {:.4f} s'.format(time() - t)) # elapsed time +print("for loop: {:.4f} s".format(time() - t)) # elapsed time t = time() # start time for the list comprehension -compr = [ - divmod(a, b) for a in range(1, mx) for b in range(a, mx)] -print('list comprehension: {:.4f} s'.format(time() - t)) +compr = [divmod(a, b) for a in range(1, mx) for b in range(a, mx)] +print("list comprehension: {:.4f} s".format(time() - t)) t = time() # start time for the generator expression -gener = list( - divmod(a, b) for a in range(1, mx) for b in range(a, mx)) -print('generator expression: {:.4f} s'.format(time() - t)) +gener = list(divmod(a, b) for a in range(1, mx) for b in range(a, mx)) +print("generator expression: {:.4f} s".format(time() - t)) # verify correctness of results and number of items in each list -print( - floop == compr == gener, len(floop), len(compr), len(gener) -) +print(floop == compr == gener, len(floop), len(compr), len(gener)) diff --git a/ch05/pythagorean.triple.comprehension.py b/ch05/pythagorean.triple.comprehension.py index 8acf967..d91de1d 100644 --- a/ch05/pythagorean.triple.comprehension.py +++ b/ch05/pythagorean.triple.comprehension.py @@ -1,9 +1,10 @@ # pythagorean.triple.comprehension.py from math import sqrt + # this step is the same as before mx = 10 -triples = [(a, b, sqrt(a**2 + b**2)) - for a in range(1, mx) for b in range(a, mx)] +triples = [(a, b, sqrt(a**2 + b**2)) for a in range(1, mx) for b in range(a, mx)] + # here we combine filter and map in one CLEAN list comprehension triples = [(a, b, int(c)) for a, b, c in triples if c.is_integer()] diff --git a/ch05/pythagorean.triple.generation.for.py b/ch05/pythagorean.triple.generation.for.py index b2c3511..630088f 100644 --- a/ch05/pythagorean.triple.generation.for.py +++ b/ch05/pythagorean.triple.generation.for.py @@ -3,15 +3,15 @@ def gen_triples(N): - for m in range(1, int(N**.5) + 1): # 1 - for n in range(1, m): # 2 - if (m - n) % 2 and gcd(m, n) == 1: # 3 - c = m**2 + n**2 # 4 - if c <= N: # 5 - a = m**2 - n**2 # 6 - b = 2 * m * n # 7 - yield (a, b, c) # 8 + for m in range(1, int(N**0.5) + 1): # 1 + for n in range(1, m): # 2 + if (m - n) % 2 and gcd(m, n) == 1: # 3 + c = m**2 + n**2 # 4 + if c <= N: # 5 + a = m**2 - n**2 # 6 + b = 2 * m * n # 7 + yield (a, b, c) # 8 -triples = sorted(gen_triples(50), key=sum) # 9 +triples = sorted(gen_triples(50), key=sum) # 9 print(triples) diff --git a/ch05/pythagorean.triple.generation.py b/ch05/pythagorean.triple.generation.py index bd01dbc..c9238f5 100644 --- a/ch05/pythagorean.triple.generation.py +++ b/ch05/pythagorean.triple.generation.py @@ -4,13 +4,18 @@ N = 50 -triples = sorted( # 1 - ((a, b, c) for a, b, c in ( # 2 - ((m**2 - n**2), (2 * m * n), (m**2 + n**2)) # 3 - for m in range(1, int(N**.5) + 1) # 4 - for n in range(1, m) # 5 - if (m - n) % 2 and gcd(m, n) == 1 # 6 - ) if c <= N), key=sum # 7 +triples = sorted( # 1 + ( + (a, b, c) + for a, b, c in ( # 2 + ((m**2 - n**2), (2 * m * n), (m**2 + n**2)) # 3 + for m in range(1, int(N**0.5) + 1) # 4 + for n in range(1, m) # 5 + if (m - n) % 2 and gcd(m, n) == 1 # 6 + ) + if c <= N + ), + key=sum, # 7 ) diff --git a/ch05/pythagorean.triple.int.py b/ch05/pythagorean.triple.int.py index d43bf4a..cc15ed1 100644 --- a/ch05/pythagorean.triple.int.py +++ b/ch05/pythagorean.triple.int.py @@ -2,12 +2,10 @@ from math import sqrt mx = 10 -triples = [(a, b, sqrt(a**2 + b**2)) - for a in range(1, mx) for b in range(a, mx)] +triples = [(a, b, sqrt(a**2 + b**2)) for a in range(1, mx) for b in range(a, mx)] triples = filter(lambda triple: triple[2].is_integer(), triples) # this will make the third number in the tuples integer -triples = list( - map(lambda triple: triple[:2] + (int(triple[2]), ), triples)) +triples = list(map(lambda triple: triple[:2] + (int(triple[2]),), triples)) print(triples) # prints: [(3, 4, 5), (6, 8, 10)] diff --git a/ch05/pythagorean.triple.py b/ch05/pythagorean.triple.py index 66da8ec..2efc18b 100644 --- a/ch05/pythagorean.triple.py +++ b/ch05/pythagorean.triple.py @@ -3,10 +3,8 @@ # this will generate all possible pairs mx = 10 -triples = [(a, b, sqrt(a**2 + b**2)) - for a in range(1, mx) for b in range(a, mx)] +triples = [(a, b, sqrt(a**2 + b**2)) for a in range(1, mx) for b in range(a, mx)] # this will filter out all non pythagorean triples -triples = list( - filter(lambda triple: triple[2].is_integer(), triples)) +triples = list(filter(lambda triple: triple[2].is_integer(), triples)) print(triples) # prints: [(3, 4, 5.0), (6, 8, 10.0)] diff --git a/ch05/pythagorean.triple.walrus.py b/ch05/pythagorean.triple.walrus.py index 78997b9..853f1c3 100644 --- a/ch05/pythagorean.triple.walrus.py +++ b/ch05/pythagorean.triple.walrus.py @@ -1,10 +1,14 @@ # pythagorean.triple.walrus.py from math import sqrt + # this step is the same as before mx = 10 # We can combine generating and filtering in one comprehension -triples = [(a, b, int(c)) - for a in range(1, mx) for b in range(a, mx) - if (c := sqrt(a**2 + b**2)).is_integer()] +triples = [ + (a, b, int(c)) + for a in range(1, mx) + for b in range(a, mx) + if (c := sqrt(a**2 + b**2)).is_integer() +] print(triples) # prints: [(3, 4, 5), (6, 8, 10)] diff --git a/ch05/scopes.noglobal.py b/ch05/scopes.noglobal.py index 723a60b..8f22d24 100644 --- a/ch05/scopes.noglobal.py +++ b/ch05/scopes.noglobal.py @@ -1,3 +1,3 @@ # scopes.noglobal.py ex1 = [A for A in range(5)] -print(A) # breaks: NameError: name 'A' is not defined +# print(A) # breaks: NameError: name 'A' is not defined diff --git a/ch05/set.comprehensions.py b/ch05/set.comprehensions.py index 68e5327..4f9208d 100644 --- a/ch05/set.comprehensions.py +++ b/ch05/set.comprehensions.py @@ -1,5 +1,5 @@ # set.comprehensions.py -word = 'Hello' +word = "Hello" letters1 = {c for c in word} letters2 = set(c for c in word) diff --git a/ch05/squares.comprehension.py b/ch05/squares.comprehension.py index 46735a2..6b16879 100644 --- a/ch05/squares.comprehension.py +++ b/ch05/squares.comprehension.py @@ -1,5 +1,5 @@ # squares.comprehension.py # This is not a valid Python module - Don't run it. ->>> [n ** 2 for n in range(10)] +[n**2 for n in range(10)] [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] diff --git a/ch05/squares.map.py b/ch05/squares.map.py index 57c447a..6ccbc14 100644 --- a/ch05/squares.map.py +++ b/ch05/squares.map.py @@ -2,14 +2,14 @@ # This is not a valid Python module - Don't run it. # If you code like this you are not a Python dev! ;) ->>> squares = [] ->>> for n in range(10): -... squares.append(n ** 2) -... ->>> squares -[0, 1, 4, 9, 16, 25, 36, 49, 64, 81] +# >>> squares = [] +# >>> for n in range(10): +# ... squares.append(n ** 2) +# ... +# >>> squares +# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] -# This is better, one line, nice and readable ->>> squares = map(lambda n: n**2, range(10)) ->>> list(squares) -[0, 1, 4, 9, 16, 25, 36, 49, 64, 81] +# # This is better, one line, nice and readable +# >>> squares = map(lambda n: n**2, range(10)) +# >>> list(squares) +# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] diff --git a/ch05/squares.py b/ch05/squares.py index 81d0d18..61ac13c 100644 --- a/ch05/squares.py +++ b/ch05/squares.py @@ -1,6 +1,7 @@ # squares.py def square1(n): - return n ** 2 # squaring through the power operator + return n**2 # squaring through the power operator + def square2(n): return n * n # squaring through multiplication diff --git a/ch05/sum.example.py b/ch05/sum.example.py index 068d664..bcbc0c3 100644 --- a/ch05/sum.example.py +++ b/ch05/sum.example.py @@ -3,4 +3,4 @@ s2 = sum((n**2 for n in range(10**6))) s3 = sum(n**2 for n in range(10**6)) -print(s1==s2==s3) +print(s1 == s2 == s3) diff --git a/ch05/zip.grades.py b/ch05/zip.grades.py index 16180d6..f8e5f7c 100644 --- a/ch05/zip.grades.py +++ b/ch05/zip.grades.py @@ -1,9 +1,10 @@ # zip.grades.py # This is not a valid Python module - Don't run it. ->>> grades = [18, 23, 30, 27] ->>> avgs = [22, 21, 29, 24] ->>> list(zip(avgs, grades)) -[(22, 18), (21, 23), (29, 30), (24, 27)] ->>> list(map(lambda *a: a, avgs, grades)) # equivalent to zip -[(22, 18), (21, 23), (29, 30), (24, 27)] + +grades = [18, 23, 30, 27] +avgs = [22, 21, 29, 24] + +list(zip(avgs, grades)) + +list(map(lambda *a: a, avgs, grades)) # equivalent to zip diff --git a/ch06/decorators/decorators.factory.py b/ch06/decorators/decorators.factory.py index 3088dc6..5d3d9f4 100644 --- a/ch06/decorators/decorators.factory.py +++ b/ch06/decorators/decorators.factory.py @@ -1,32 +1,37 @@ # decorators/decorators.factory.py from functools import wraps + def max_result(threshold): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): result = func(*args, **kwargs) if result > threshold: - print( - f'Result is too big ({result}). ' - f'Max allowed is {threshold}.' - ) + print(f"Result is too big ({result}). " + f"Max allowed is {threshold}.") return result + return wrapper + return decorator + @max_result(75) def cube(n): - return n ** 3 + return n**3 + @max_result(100) def square(n): - return n ** 2 + return n**2 + @max_result(1000) def multiply(a, b): return a * b + print(cube(5)) diff --git a/ch06/decorators/syntax.py b/ch06/decorators/syntax.py index 01d988c..5ec7b48 100644 --- a/ch06/decorators/syntax.py +++ b/ch06/decorators/syntax.py @@ -2,39 +2,39 @@ # This is not a valid Python module - Don't run it. # ONE DECORATOR -def func(arg1, arg2, ...): - pass +# def func(arg1, arg2, ...): +# pass -func = decorator(func) +# func = decorator(func) -# is equivalent to the following: +# # is equivalent to the following: -@decorator -def func(arg1, arg2, ...): - pass +# @decorator +# def func(arg1, arg2, ...): +# pass -# TWO DECORATORS -def func(arg1, arg2, ...): - pass +# # TWO DECORATORS +# def func(arg1, arg2, ...): +# pass -func = deco1(deco2(func)) +# func = deco1(deco2(func)) -# is equivalent to the following: +# # is equivalent to the following: -@deco1 -@deco2 -def func(arg1, arg2, ...): - pass +# @deco1 +# @deco2 +# def func(arg1, arg2, ...): +# pass -# DECORATOR WITH ARGUMENTS -def func(arg1, arg2, ...): - pass +# # DECORATOR WITH ARGUMENTS +# def func(arg1, arg2, ...): +# pass -func = decoarg(arg_a, arg_b)(func) +# func = decoarg(arg_a, arg_b)(func) -# is equivalent to the following: +# # is equivalent to the following: -@decoarg(arg_a, arg_b) -def func(arg1, arg2, ...): - pass +# @decoarg(arg_a, arg_b) +# def func(arg1, arg2, ...): +# pass diff --git a/ch06/decorators/time.measure.arguments.py b/ch06/decorators/time.measure.arguments.py index f75cacf..cadb2f2 100644 --- a/ch06/decorators/time.measure.arguments.py +++ b/ch06/decorators/time.measure.arguments.py @@ -5,10 +5,12 @@ def f(sleep_time=0.1): sleep(sleep_time) + def measure(func, *args, **kwargs): t = time() func(*args, **kwargs) - print(func.__name__, 'took:', time() - t) + print(func.__name__, "took:", time() - t) + measure(f, sleep_time=0.3) # f took: 0.30056095123291016 measure(f, 0.2) # f took: 0.2033553123474121 diff --git a/ch06/decorators/time.measure.deco1.py b/ch06/decorators/time.measure.deco1.py index fb0d325..611d4c2 100644 --- a/ch06/decorators/time.measure.deco1.py +++ b/ch06/decorators/time.measure.deco1.py @@ -1,16 +1,20 @@ # decorators/time.measure.deco1.py from time import sleep, time + def f(sleep_time=0.1): sleep(sleep_time) + def measure(func): def wrapper(*args, **kwargs): t = time() func(*args, **kwargs) - print(func.__name__, 'took:', time() - t) + print(func.__name__, "took:", time() - t) + return wrapper + f = measure(f) # decoration point f(0.2) # f took: 0.20372915267944336 diff --git a/ch06/decorators/time.measure.deco2.py b/ch06/decorators/time.measure.deco2.py index dbb8454..eb9886d 100644 --- a/ch06/decorators/time.measure.deco2.py +++ b/ch06/decorators/time.measure.deco2.py @@ -8,15 +8,16 @@ def measure(func): def wrapper(*args, **kwargs): t = time() func(*args, **kwargs) - print(func.__name__, 'took:', time() - t) + print(func.__name__, "took:", time() - t) + return wrapper @measure def f(sleep_time=0.1): - """I'm a cat. I love to sleep! """ + """I'm a cat. I love to sleep!""" sleep(sleep_time) f(sleep_time=0.3) # f took: 0.3010902404785156 -print(f.__name__, ':', f.__doc__) # f : I'm a cat. I love to sleep! +print(f.__name__, ":", f.__doc__) # f : I'm a cat. I love to sleep! diff --git a/ch06/decorators/time.measure.dry.py b/ch06/decorators/time.measure.dry.py index 9e93c80..cab9400 100644 --- a/ch06/decorators/time.measure.dry.py +++ b/ch06/decorators/time.measure.dry.py @@ -3,15 +3,18 @@ def f(): - sleep(.3) + sleep(0.3) + def g(): - sleep(.5) + sleep(0.5) + def measure(func): t = time() func() - print(func.__name__, 'took:', time() - t) + print(func.__name__, "took:", time() - t) + measure(f) # f took: 0.30434322357177734 measure(g) # g took: 0.5048270225524902 diff --git a/ch06/decorators/time.measure.start.py b/ch06/decorators/time.measure.start.py index b376495..6f9387c 100644 --- a/ch06/decorators/time.measure.start.py +++ b/ch06/decorators/time.measure.start.py @@ -3,16 +3,17 @@ def f(): - sleep(.3) + sleep(0.3) + def g(): - sleep(.5) + sleep(0.5) t = time() f() -print('f took:', time() - t) # f took: 0.3001396656036377 +print("f took:", time() - t) # f took: 0.3001396656036377 t = time() g() -print('g took:', time() - t) # g took: 0.5039339065551758 +print("g took:", time() - t) # g took: 0.5039339065551758 diff --git a/ch06/decorators/two.decorators.py b/ch06/decorators/two.decorators.py index 8a90a29..46dfb1d 100644 --- a/ch06/decorators/two.decorators.py +++ b/ch06/decorators/two.decorators.py @@ -2,31 +2,34 @@ from time import time from functools import wraps + def measure(func): @wraps(func) def wrapper(*args, **kwargs): t = time() result = func(*args, **kwargs) - print(func.__name__, 'took:', time() - t) + print(func.__name__, "took:", time() - t) return result + return wrapper + def max_result(func): @wraps(func) def wrapper(*args, **kwargs): result = func(*args, **kwargs) if result > 100: - print( - f'Result is too big ({result}). ' - 'Max allowed is 100.' - ) + print(f"Result is too big ({result}). " "Max allowed is 100.") return result + return wrapper + @measure @max_result def cube(n): - return n ** 3 + return n**3 + print(cube(2)) print(cube(5)) diff --git a/ch06/iterators/iterator.py b/ch06/iterators/iterator.py index af0d4e9..1a0bed9 100644 --- a/ch06/iterators/iterator.py +++ b/ch06/iterators/iterator.py @@ -1,10 +1,9 @@ # iterators/iterator.py class OddEven: - def __init__(self, data): self._data = data - self.indexes = (list(range(0, len(data), 2)) + - list(range(1, len(data), 2))) + self.indexes = list(range(0, len(data), 2)) + list(range(1, len(data), + 2)) def __iter__(self): return self @@ -14,10 +13,11 @@ def __next__(self): return self._data[self.indexes.pop(0)] raise StopIteration -oddeven = OddEven('ThIsIsCoOl!') -print(''.join(c for c in oddeven)) # TIICO!hssol -oddeven = OddEven('CiAo') # or manually... +oddeven = OddEven("ThIsIsCoOl!") +print("".join(c for c in oddeven)) # TIICO!hssol + +oddeven = OddEven("CiAo") # or manually... it = iter(oddeven) # this calls oddeven.__iter__ internally print(next(it)) # C print(next(it)) # A @@ -26,17 +26,17 @@ def __next__(self): # make sure it works correctly with edge cases -oddeven = OddEven('') -print(' '.join(c for c in oddeven)) +oddeven = OddEven("") +print(" ".join(c for c in oddeven)) -oddeven = OddEven('A') -print(' '.join(c for c in oddeven)) +oddeven = OddEven("A") +print(" ".join(c for c in oddeven)) -oddeven = OddEven('Ab') -print(' '.join(c for c in oddeven)) +oddeven = OddEven("Ab") +print(" ".join(c for c in oddeven)) -oddeven = OddEven('AbC') -print(' '.join(c for c in oddeven)) +oddeven = OddEven("AbC") +print(" ".join(c for c in oddeven)) """ diff --git a/ch06/oop/cached.property.py b/ch06/oop/cached.property.py index 28f3142..0a10785 100644 --- a/ch06/oop/cached.property.py +++ b/ch06/oop/cached.property.py @@ -22,7 +22,7 @@ def perform_query(self, **kwargs): class ManualCacheManager: @property def client(self): - if not hasattr(self, '_client'): + if not hasattr(self, "_client"): self._client = Client() return self._client @@ -41,7 +41,7 @@ def perform_query(self, **kwargs): manager = CachedPropertyManager() manager.perform_query(object_id=42) -manager.perform_query(name_ilike='%Python%') +manager.perform_query(name_ilike="%Python%") del manager.client # This causes a new Client on next call manager.perform_query(age_gte=18) diff --git a/ch06/oop/class.attribute.shadowing.py b/ch06/oop/class.attribute.shadowing.py index 0edc60c..96bcfb8 100644 --- a/ch06/oop/class.attribute.shadowing.py +++ b/ch06/oop/class.attribute.shadowing.py @@ -3,6 +3,7 @@ class Point: x = 10 y = 7 + p = Point() print(p.x) # 10 (from class attribute) print(p.y) # 7 (from class attribute) @@ -30,7 +31,8 @@ class Point: 10 3 Traceback (most recent call last): - File "/Users/fab/srv/lpp3e/v3/ch06/oop/class.attribute.shadowing.py", line 20, in + File "/Users/fab/srv/lpp3e/v3/ch06/oop/class.attribute.shadowing.py", + line 20, in print(Point.z) AttributeError: type object 'Point' has no attribute 'z' """ diff --git a/ch06/oop/class.init.py b/ch06/oop/class.init.py index 2b76f43..b8095ce 100644 --- a/ch06/oop/class.init.py +++ b/ch06/oop/class.init.py @@ -7,6 +7,7 @@ def __init__(self, side_a, side_b): def area(self): return self.side_a * self.side_b + r1 = Rectangle(10, 4) print(r1.side_a, r1.side_b) # 10 4 print(r1.area()) # 40 diff --git a/ch06/oop/class.issubclass.isinstance.py b/ch06/oop/class.issubclass.isinstance.py index 0b85fe6..421b983 100644 --- a/ch06/oop/class.issubclass.isinstance.py +++ b/ch06/oop/class.issubclass.isinstance.py @@ -5,13 +5,13 @@ car = Car() racecar = RaceCar() f1car = F1Car() -cars = [(car, 'car'), (racecar, 'racecar'), (f1car, 'f1car')] +cars = [(car, "car"), (racecar, "racecar"), (f1car, "f1car")] car_classes = [Car, RaceCar, F1Car] for car, car_name in cars: for class_ in car_classes: belongs = isinstance(car, class_) - msg = 'is a' if belongs else 'is not a' + msg = "is a" if belongs else "is not a" print(car_name, msg, class_.__name__) """ Prints: @@ -30,13 +30,12 @@ f1car is a F1Car """ -print('-' * 60) +print("-" * 60) for class1 in car_classes: for class2 in car_classes: is_subclass = issubclass(class1, class2) - msg = '{0} a subclass of'.format( - 'is' if is_subclass else 'is not') + msg = "{0} a subclass of".format("is" if is_subclass else "is not") print(class1.__name__, msg, class2.__name__) """ Prints: diff --git a/ch06/oop/class.methods.factory.py b/ch06/oop/class.methods.factory.py index 1831586..0c8237c 100644 --- a/ch06/oop/class.methods.factory.py +++ b/ch06/oop/class.methods.factory.py @@ -1,6 +1,5 @@ # oop/class.methods.factory.py class Point: - def __init__(self, x, y): self.x = x self.y = y diff --git a/ch06/oop/class.methods.split.py b/ch06/oop/class.methods.split.py index fa2ea2f..916c9bd 100644 --- a/ch06/oop/class.methods.split.py +++ b/ch06/oop/class.methods.split.py @@ -1,6 +1,5 @@ # oop/class.methods.split.py class StringUtil: - @classmethod def is_palindrome(cls, s, case_insensitive=True): s = cls._strip_string(s) @@ -11,12 +10,12 @@ def is_palindrome(cls, s, case_insensitive=True): @staticmethod def _strip_string(s): - return ''.join(c for c in s if c.isalnum()) + return "".join(c for c in s if c.isalnum()) @staticmethod def _is_palindrome(s): for c in range(len(s) // 2): - if s[c] != s[-c -1]: + if s[c] != s[-c - 1]: return False return True @@ -24,8 +23,9 @@ def _is_palindrome(s): def get_unique_words(sentence): return set(sentence.split()) -print(StringUtil.is_palindrome('A nut for a jar of tuna')) # True -print(StringUtil.is_palindrome('A nut for a jar of beans')) # False + +print(StringUtil.is_palindrome("A nut for a jar of tuna")) # True +print(StringUtil.is_palindrome("A nut for a jar of beans")) # False """ diff --git a/ch06/oop/class.namespaces.py b/ch06/oop/class.namespaces.py index f9e7f15..c3aff58 100644 --- a/ch06/oop/class.namespaces.py +++ b/ch06/oop/class.namespaces.py @@ -1,6 +1,6 @@ # oop/class.namespaces.py class Person: - species = 'Human' + species = "Human" print(Person.species) # Human @@ -14,8 +14,8 @@ class Person: Person.alive = False print(man.alive) # False (inherited) -man.name = 'Darth' -man.surname = 'Vader' +man.name = "Darth" +man.surname = "Vader" print(man.name, man.surname) # Darth Vader print(Person.name) @@ -34,7 +34,8 @@ class Person: False Darth Vader Traceback (most recent call last): - File "/Users/fab/srv/lpp3e/v3/ch06/oop/class.namespaces.py", line 21, in + File "/Users/fab/srv/lpp3e/v3/ch06/oop/class.namespaces.py", line 21, + in print(Person.name) AttributeError: type object 'Person' has no attribute 'name' """ diff --git a/ch06/oop/class.price.py b/ch06/oop/class.price.py index e17ccc8..9c0e5eb 100644 --- a/ch06/oop/class.price.py +++ b/ch06/oop/class.price.py @@ -4,6 +4,7 @@ def final_price(self, vat, discount=0): """Returns price after applying vat and fixed discount.""" return (self.net_price * (100 + vat) / 100) - discount + p1 = Price() p1.net_price = 100 print(Price.final_price(p1, 20, 10)) # 110 (100 * 1.2 - 10) diff --git a/ch06/oop/class.self.py b/ch06/oop/class.self.py index 594fc03..6b9fa23 100644 --- a/ch06/oop/class.self.py +++ b/ch06/oop/class.self.py @@ -1,8 +1,10 @@ # oop/class.self.py class Square: side = 8 + def area(self): # self is a reference to an instance - return self.side ** 2 + return self.side**2 + sq = Square() print(sq.area()) # 64 (side is found on the class) @@ -12,7 +14,6 @@ def area(self): # self is a reference to an instance print(sq.area()) # 100 (side is found on the instance) - """ $ python class.self.py 64 diff --git a/ch06/oop/class_inheritance.py b/ch06/oop/class_inheritance.py index 7e3d074..21ee683 100644 --- a/ch06/oop/class_inheritance.py +++ b/ch06/oop/class_inheritance.py @@ -1,4 +1,4 @@ -# oop/class_inheritance.py +# oop/class.inheritance.py class Engine: def start(self): pass @@ -6,12 +6,15 @@ def start(self): def stop(self): pass + class ElectricEngine(Engine): # Is-A Engine pass + class V8Engine(Engine): # Is-A Engine pass + class Car: engine_cls = Engine @@ -20,25 +23,28 @@ def __init__(self): def start(self): print( - 'Starting engine {0} for car {1}... Wroom, wroom!' - .format( - self.engine.__class__.__name__, - self.__class__.__name__) + "Starting engine {0} for car {1}... Wroom, wroom!".format( + self.engine.__class__.__name__, self.__class__.__name__ + ) ) self.engine.start() def stop(self): self.engine.stop() + class RaceCar(Car): # Is-A Car engine_cls = V8Engine + class CityCar(Car): # Is-A Car engine_cls = ElectricEngine + class F1Car(RaceCar): # Is-A RaceCar and also Is-A Car pass # engine_cls same as parent + car = Car() racecar = RaceCar() citycar = CityCar() diff --git a/ch06/oop/dataclass.py b/ch06/oop/dataclass.py index be9dfda..938e2fd 100644 --- a/ch06/oop/dataclass.py +++ b/ch06/oop/dataclass.py @@ -4,16 +4,17 @@ @dataclass class Body: - '''Class to represent a physical body.''' + """Class to represent a physical body.""" + name: str - mass: float = 0. # Kg - speed: float = 1. # m/s + mass: float = 0.0 # Kg + speed: float = 1.0 # m/s def kinetic_energy(self) -> float: - return (self.mass * self.speed ** 2) / 2 + return (self.mass * self.speed**2) / 2 -body = Body('Ball', 19, 3.1415) +body = Body("Ball", 19, 3.1415) print(body.kinetic_energy()) # 93.755711375 Joule print(body) # Body(name='Ball', mass=19, speed=3.1415) diff --git a/ch06/oop/mro.py b/ch06/oop/mro.py index 96640df..ec2abbb 100644 --- a/ch06/oop/mro.py +++ b/ch06/oop/mro.py @@ -1,12 +1,15 @@ # oop/mro.py class A: - label = 'a' + label = "a" + class B(A): pass # was: label = 'b' + class C(A): - label = 'c' + label = "c" + class D(B, C): pass diff --git a/ch06/oop/mro.simple.py b/ch06/oop/mro.simple.py index 568d371..ca9d0b0 100644 --- a/ch06/oop/mro.simple.py +++ b/ch06/oop/mro.simple.py @@ -1,16 +1,20 @@ # oop/mro.simple.py class A: - label = 'a' + label = "a" + class B(A): - label = 'b' + label = "b" + class C(A): - label = 'c' + label = "c" + class D(B, C): pass + d = D() print(d.label) # Hypothetically this could be either 'b' or 'c' print(d.__class__.mro()) # notice another way to get the MRO diff --git a/ch06/oop/multiple.inheritance.py b/ch06/oop/multiple.inheritance.py index 44f4feb..6e682bf 100644 --- a/ch06/oop/multiple.inheritance.py +++ b/ch06/oop/multiple.inheritance.py @@ -1,6 +1,6 @@ # oop/multiple.inheritance.py class Shape: - geometric_type = 'Generic Shape' + geometric_type = "Generic Shape" def area(self): # This acts as placeholder for the interface raise NotImplementedError @@ -10,32 +10,31 @@ def get_geometric_type(self): class Plotter: - def plot(self, ratio, topleft): # Imagine some nice plotting logic here... - print('Plotting at {}, ratio {}.'.format( - topleft, ratio)) + print("Plotting at {}, ratio {}.".format(topleft, ratio)) class Polygon(Shape, Plotter): # base class for polygons - geometric_type = 'Polygon' + geometric_type = "Polygon" + class RegularPolygon(Polygon): # Is-A Polygon - geometric_type = 'Regular Polygon' + geometric_type = "Regular Polygon" def __init__(self, side): self.side = side class RegularHexagon(RegularPolygon): # Is-A RegularPolygon - geometric_type = 'RegularHexagon' + geometric_type = "RegularHexagon" def area(self): - return 1.5 * (3 ** .5 * self.side ** 2) + return 1.5 * (3**0.5 * self.side**2) class Square(RegularPolygon): # Is-A RegularPolygon - geometric_type = 'Square' + geometric_type = "Square" def area(self): return self.side * self.side diff --git a/ch06/oop/operator.overloading.py b/ch06/oop/operator.overloading.py index 00ff5f4..8789145 100644 --- a/ch06/oop/operator.overloading.py +++ b/ch06/oop/operator.overloading.py @@ -7,14 +7,14 @@ def __len__(self): return len(self._s) def __bool__(self): - return '42' in self._s + return "42" in self._s -weird = Weird('Hello! I am 9 years old!') +weird = Weird("Hello! I am 9 years old!") print(len(weird)) # 24 print(bool(weird)) # False -weird2 = Weird('Hello! I am 42 years old!') +weird2 = Weird("Hello! I am 42 years old!") print(len(weird2)) # 25 print(bool(weird2)) # True diff --git a/ch06/oop/private.attrs.fixed.py b/ch06/oop/private.attrs.fixed.py index ee2c1a0..025c4ac 100644 --- a/ch06/oop/private.attrs.fixed.py +++ b/ch06/oop/private.attrs.fixed.py @@ -4,18 +4,19 @@ def __init__(self, factor): self.__factor = factor def op1(self): - print('Op1 with factor {}...'.format(self.__factor)) + print("Op1 with factor {}...".format(self.__factor)) + class B(A): def op2(self, factor): self.__factor = factor - print('Op2 with factor {}...'.format(self.__factor)) + print("Op2 with factor {}...".format(self.__factor)) obj = B(100) -obj.op1() # Op1 with factor 100... +obj.op1() # Op1 with factor 100... obj.op2(42) # Op2 with factor 42... -obj.op1() # Op1 with factor 100... <- Wohoo! Now it's GOOD! +obj.op1() # Op1 with factor 100... <- Wohoo! Now it's GOOD! print(obj.__dict__.keys()) # dict_keys(['_A__factor', '_B__factor']) diff --git a/ch06/oop/private.attrs.py b/ch06/oop/private.attrs.py index 1e55fd9..dc268b0 100644 --- a/ch06/oop/private.attrs.py +++ b/ch06/oop/private.attrs.py @@ -4,18 +4,19 @@ def __init__(self, factor): self._factor = factor def op1(self): - print('Op1 with factor {}...'.format(self._factor)) + print("Op1 with factor {}...".format(self._factor)) + class B(A): def op2(self, factor): self._factor = factor - print('Op2 with factor {}...'.format(self._factor)) + print("Op2 with factor {}...".format(self._factor)) obj = B(100) -obj.op1() # Op1 with factor 100... +obj.op1() # Op1 with factor 100... obj.op2(42) # Op2 with factor 42... -obj.op1() # Op1 with factor 42... <- This is BAD +obj.op1() # Op1 with factor 42... <- This is BAD print(obj.__dict__.keys()) # dict_keys(['_factor']) diff --git a/ch06/oop/property.py b/ch06/oop/property.py index 2d342ad..fe958d0 100644 --- a/ch06/oop/property.py +++ b/ch06/oop/property.py @@ -3,6 +3,7 @@ class Person: def __init__(self, age): self.age = age # anyone can modify this freely + class PersonWithAccessors: def __init__(self, age): self._age = age @@ -14,7 +15,8 @@ def set_age(self, age): if 18 <= age <= 99: self._age = age else: - raise ValueError('Age must be within [18, 99]') + raise ValueError("Age must be within [18, 99]") + class PersonPythonic: def __init__(self, age): @@ -29,13 +31,14 @@ def age(self, age): if 18 <= age <= 99: self._age = age else: - raise ValueError('Age must be within [18, 99]') + raise ValueError("Age must be within [18, 99]") + person = PersonPythonic(39) print(person.age) # 39 - Notice we access as data attribute -person.age = 42 # Notice we access as data attribute +person.age = 42 # Notice we access as data attribute print(person.age) # 42 -person.age = 100 # ValueError: Age must be within [18, 99] +person.age = 100 # ValueError: Age must be within [18, 99] """ diff --git a/ch06/oop/simplest.class.py b/ch06/oop/simplest.class.py index 3024efd..1e21fdb 100644 --- a/ch06/oop/simplest.class.py +++ b/ch06/oop/simplest.class.py @@ -1,7 +1,8 @@ # oop/simplest.class.py -class Simplest(): # when empty, the braces are optional +class Simplest: # when empty, the braces are optional pass + print(type(Simplest)) # what type is this object? simp = Simplest() # we create an instance of Simplest: simp diff --git a/ch06/oop/static.methods.py b/ch06/oop/static.methods.py index 65fd1ac..3b31bf1 100644 --- a/ch06/oop/static.methods.py +++ b/ch06/oop/static.methods.py @@ -1,15 +1,14 @@ # oop/static.methods.py class StringUtil: - @staticmethod def is_palindrome(s, case_insensitive=True): # we allow only letters and numbers - s = ''.join(c for c in s if c.isalnum()) # Study this! + s = "".join(c for c in s if c.isalnum()) # Study this! # For case insensitive comparison, we lower-case s if case_insensitive: s = s.lower() for c in range(len(s) // 2): - if s[c] != s[-c -1]: + if s[c] != s[-c - 1]: return False return True @@ -17,16 +16,19 @@ def is_palindrome(s, case_insensitive=True): def get_unique_words(sentence): return set(sentence.split()) -print(StringUtil.is_palindrome( - 'Radar', case_insensitive=False)) # False: Case Sensitive -print(StringUtil.is_palindrome('A nut for a jar of tuna')) # True -print(StringUtil.is_palindrome('Never Odd, Or Even!')) # True -print(StringUtil.is_palindrome( - 'In Girum Imus Nocte Et Consumimur Igni') # Latin! Show-off! + +print( + StringUtil.is_palindrome("Radar", case_insensitive=False) +) # False: Case Sensitive +print(StringUtil.is_palindrome("A nut for a jar of tuna")) # True +print(StringUtil.is_palindrome("Never Odd, Or Even!")) # True +print( + StringUtil.is_palindrome( + "In Girum Imus Nocte Et Consumimur Igni" + ) # Latin! Show-off! ) # True -print(StringUtil.get_unique_words( - 'I love palindromes. I really really love them!')) +print(StringUtil.get_unique_words("I love palindromes. I really really love them!")) # {'them!', 'palindromes.', 'I', 'really', 'love'} diff --git a/ch06/oop/super.duplication.py b/ch06/oop/super.duplication.py index 5096815..8b3d165 100644 --- a/ch06/oop/super.duplication.py +++ b/ch06/oop/super.duplication.py @@ -1,6 +1,5 @@ # oop/super.duplication.py class Book: - def __init__(self, title, publisher, pages): self.title = title self.publisher = publisher @@ -8,7 +7,6 @@ def __init__(self, title, publisher, pages): class Ebook(Book): - def __init__(self, title, publisher, pages, format_): self.title = title self.publisher = publisher diff --git a/ch06/oop/super.explicit.py b/ch06/oop/super.explicit.py index d8093e3..da3bbee 100644 --- a/ch06/oop/super.explicit.py +++ b/ch06/oop/super.explicit.py @@ -1,6 +1,5 @@ # oop/super.explicit.py class Book: - def __init__(self, title, publisher, pages): self.title = title self.publisher = publisher @@ -8,14 +7,12 @@ def __init__(self, title, publisher, pages): class Ebook(Book): - def __init__(self, title, publisher, pages, format_): Book.__init__(self, title, publisher, pages) self.format_ = format_ -ebook = Ebook( - 'Learn Python Programming', 'Packt Publishing', 500, 'PDF') +ebook = Ebook("Learn Python Programming", "Packt Publishing", 500, "PDF") print(ebook.title) # Learn Python Programming print(ebook.publisher) # Packt Publishing print(ebook.pages) # 500 diff --git a/ch06/oop/super.implicit.py b/ch06/oop/super.implicit.py index 9387598..0f87be9 100644 --- a/ch06/oop/super.implicit.py +++ b/ch06/oop/super.implicit.py @@ -1,6 +1,5 @@ # oop/super.implicit.py class Book: - def __init__(self, title, publisher, pages): self.title = title self.publisher = publisher @@ -8,7 +7,6 @@ def __init__(self, title, publisher, pages): class Ebook(Book): - def __init__(self, title, publisher, pages, format_): super().__init__(title, publisher, pages) # Another way to do the same thing is: @@ -16,8 +14,7 @@ def __init__(self, title, publisher, pages, format_): self.format_ = format_ -ebook = Ebook( - 'Learn Python Programming', 'Packt Publishing', 500, 'PDF') +ebook = Ebook("Learn Python Programming", "Packt Publishing", 500, "PDF") print(ebook.title) # Learn Python Programming print(ebook.publisher) # Packt Publishing print(ebook.pages) # 500 diff --git a/ch07/context/decimal.prec.py b/ch07/context/decimal.prec.py index ca89cac..bcd3eb4 100644 --- a/ch07/context/decimal.prec.py +++ b/ch07/context/decimal.prec.py @@ -1,5 +1,6 @@ # context/decimal.prec.py from decimal import Context, Decimal, getcontext, setcontext +from decimal import localcontext one = Decimal("1") three = Decimal("3") @@ -34,8 +35,6 @@ print(one / three) -from decimal import localcontext - with localcontext(Context(prec=5)) as ctx: print(ctx) print(one / three) diff --git a/ch07/context/generator.py b/ch07/context/generator.py index 780ccca..1ff397a 100644 --- a/ch07/context/generator.py +++ b/ch07/context/generator.py @@ -1,6 +1,7 @@ # context/generator.py from contextlib import contextmanager + @contextmanager def my_context_manager(): print("Entering 'with' context") diff --git a/ch07/exceptions/first.example.py b/ch07/exceptions/first.example.py index 530781f..26dddb4 100644 --- a/ch07/exceptions/first.example.py +++ b/ch07/exceptions/first.example.py @@ -2,30 +2,30 @@ # This is not a valid Python module - Don't run it. # a few examples of exceptions ->>> gen = (n for n in range(2)) ->>> next(gen) -0 ->>> next(gen) -1 ->>> next(gen) -Traceback (most recent call last): - File "", line 1, in -StopIteration ->>> print(undefined_name) -Traceback (most recent call last): - File "", line 1, in -NameError: name 'undefined_name' is not defined ->>> mylist = [1, 2, 3] ->>> mylist[5] -Traceback (most recent call last): - File "", line 1, in -IndexError: list index out of range ->>> mydict = {'a': 'A', 'b': 'B'} ->>> mydict['c'] -Traceback (most recent call last): - File "", line 1, in -KeyError: 'c' ->>> 1 / 0 -Traceback (most recent call last): - File "", line 1, in -ZeroDivisionError: division by zero +gen = (n for n in range(2)) +next(gen) + +next(gen) + +next(gen) +# Traceback (most recent call last): +# File "", line 1, in +# StopIteration +# >>> print(undefined_name) +# Traceback (most recent call last): +# File "", line 1, in +# NameError: name 'undefined_name' is not defined +# >>> mylist = [1, 2, 3] +# >>> mylist[5] +# Traceback (most recent call last): +# File "", line 1, in +# IndexError: list index out of range +# >>> mydict = {'a': 'A', 'b': 'B'} +# >>> mydict['c'] +# Traceback (most recent call last): +# File "", line 1, in +# KeyError: 'c' +# >>> 1 / 0 +# Traceback (most recent call last): +# File "", line 1, in +# ZeroDivisionError: division by zero diff --git a/ch07/exceptions/for.loop.py b/ch07/exceptions/for.loop.py index 33bf060..09e826c 100644 --- a/ch07/exceptions/for.loop.py +++ b/ch07/exceptions/for.loop.py @@ -2,9 +2,11 @@ n = 100 found = False for a in range(n): - if found: break + if found: + break for b in range(n): - if found: break + if found: + break for c in range(n): if 42 * a + 17 * b + c == 5096: found = True @@ -14,6 +16,7 @@ class ExitLoopException(Exception): pass + try: n = 100 for a in range(n): diff --git a/ch07/exceptions/raising.py b/ch07/exceptions/raising.py index 8935dbd..d2879e0 100644 --- a/ch07/exceptions/raising.py +++ b/ch07/exceptions/raising.py @@ -1,6 +1,7 @@ # exceptions/raising.py # This is not a valid Python module - Don't run it. ->>> raise NotImplementedError("I'm afraid I can't do that") -Traceback (most recent call last): - File "", line 1, in -NotImplementedError: I'm afraid I can't do that + +raise NotImplementedError("I'm afraid I can't do that") +# Traceback (most recent call last): +# File "", line 1, in +# NotImplementedError: I'm afraid I can't do that diff --git a/ch07/exceptions/replace.py b/ch07/exceptions/replace.py index c27e663..04901be 100644 --- a/ch07/exceptions/replace.py +++ b/ch07/exceptions/replace.py @@ -1,34 +1,34 @@ # exceptions/replace.py # This is not a valid Python module - Don't run it. ->>> class NotFoundError(Exception): -... pass -... ->>> vowels = {'a': 1, 'e': 5, 'i': 9, 'o': 15, 'u': 21} ->>> try: -... pos = vowels['y'] -... except KeyError as e: -... raise NotFoundError(*e.args) -... -Traceback (most recent call last): - File "", line 2, in -KeyError: 'y' +# >>> class NotFoundError(Exception): +# ... pass +# ... +# >>> vowels = {'a': 1, 'e': 5, 'i': 9, 'o': 15, 'u': 21} +# >>> try: +# ... pos = vowels['y'] +# ... except KeyError as e: +# ... raise NotFoundError(*e.args) +# ... +# Traceback (most recent call last): +# File "", line 2, in +# KeyError: 'y' -During handling of the above exception, another exception occurred: +# During handling of the above exception, another exception occurred: -Traceback (most recent call last): - File "", line 4, in -__main__.NotFoundError: y ->>> try: -... pos = vowels['y'] -... except KeyError as e: -... raise NotFoundError(*e.args) from e -... -Traceback (most recent call last): - File "", line 2, in -KeyError: 'y' +# Traceback (most recent call last): +# File "", line 4, in +# __main__.NotFoundError: y +# >>> try: +# ... pos = vowels['y'] +# ... except KeyError as e: +# ... raise NotFoundError(*e.args) from e +# ... +# Traceback (most recent call last): +# File "", line 2, in +# KeyError: 'y' -The above exception was the direct cause of the following exception: +# The above exception was the direct cause of the following exception: -Traceback (most recent call last): - File "", line 4, in -__main__.NotFoundError: y +# Traceback (most recent call last): +# File "", line 4, in +# __main__.NotFoundError: y diff --git a/ch07/exceptions/trace.back.py b/ch07/exceptions/trace.back.py index cf1a3e2..631bd53 100644 --- a/ch07/exceptions/trace.back.py +++ b/ch07/exceptions/trace.back.py @@ -2,12 +2,13 @@ def squareroot(number): if number < 0: raise ValueError("No negative numbers please") - return number ** .5 + return number**0.5 + def quadratic(a, b, c): - d = b ** 2 - 4 * a * c - return ((-b - squareroot(d)) / (2 * a), - (-b + squareroot(d)) / (2 * a)) + d = b**2 - 4 * a * c + return ((-b - squareroot(d)) / (2 * a), (-b + squareroot(d)) / (2 * a)) + quadratic(1, 0, 1) # x**2 + 1 == 0 diff --git a/ch07/exceptions/try.syntax.py b/ch07/exceptions/try.syntax.py index a6718f2..1123fa3 100644 --- a/ch07/exceptions/try.syntax.py +++ b/ch07/exceptions/try.syntax.py @@ -1,16 +1,18 @@ # exceptions/try.syntax.py + def try_syntax(numerator, denominator): try: - print(f'In the try block: {numerator}/{denominator}') + print(f"In the try block: {numerator}/{denominator}") result = numerator / denominator except ZeroDivisionError as zde: print(zde) else: - print('The result is:', result) + print("The result is:", result) return result finally: - print('Exiting') + print("Exiting") + print(try_syntax(12, 4)) print(try_syntax(11, 0)) diff --git a/ch08/files/compression/tar.py b/ch08/files/compression/tar.py index 69af6c2..47fd8a6 100644 --- a/ch08/files/compression/tar.py +++ b/ch08/files/compression/tar.py @@ -1,11 +1,11 @@ # files/compression/tar.py import tarfile -with tarfile.open('example.tar.gz', 'w:gz') as tar: - tar.add('content1.txt') - tar.add('content2.txt') - tar.add('subfolder/content3.txt') - tar.add('subfolder/content4.txt') +with tarfile.open("example.tar.gz", "w:gz") as tar: + tar.add("content1.txt") + tar.add("content2.txt") + tar.add("subfolder/content3.txt") + tar.add("subfolder/content4.txt") -with tarfile.open('example.tar.gz', 'r:gz') as tar: - tar.extractall('extract_tar') +with tarfile.open("example.tar.gz", "r:gz") as tar: + tar.extractall("extract_tar") diff --git a/ch08/files/compression/zip.py b/ch08/files/compression/zip.py index c2d359e..c10e2f2 100644 --- a/ch08/files/compression/zip.py +++ b/ch08/files/compression/zip.py @@ -2,13 +2,13 @@ from zipfile import ZipFile -with ZipFile('example.zip', 'w') as zp: - zp.write('content1.txt') - zp.write('content2.txt') - zp.write('subfolder/content3.txt') - zp.write('subfolder/content4.txt') +with ZipFile("example.zip", "w") as zp: + zp.write("content1.txt") + zp.write("content2.txt") + zp.write("subfolder/content3.txt") + zp.write("subfolder/content4.txt") -with ZipFile('example.zip') as zp: - zp.extract('content1.txt', 'extract_zip') - zp.extract('subfolder/content3.txt', 'extract_zip') +with ZipFile("example.zip") as zp: + zp.extract("content1.txt", "extract_zip") + zp.extract("subfolder/content3.txt", "extract_zip") diff --git a/ch08/files/existence.py b/ch08/files/existence.py index f9bf973..8d5af59 100644 --- a/ch08/files/existence.py +++ b/ch08/files/existence.py @@ -1,12 +1,12 @@ # files/existence.py from pathlib import Path -p = Path('fear.txt') +p = Path("fear.txt") path = p.parent.absolute() -print(p.is_file()) # True -print(path) # /Users/fab/srv/lpp3e/ch08/files -print(path.is_dir()) # True +print(p.is_file()) # True +print(path) # /Users/fab/srv/lpp3e/ch08/files +print(path.is_dir()) # True -q = Path('/Users/fab/srv/lpp3e/ch08/files') -print(q.is_dir()) # True +q = Path(path) +print(q.is_dir()) # True diff --git a/ch08/files/listing.py b/ch08/files/listing.py index d9ff3e5..64c0876 100644 --- a/ch08/files/listing.py +++ b/ch08/files/listing.py @@ -2,11 +2,11 @@ from pathlib import Path -p = Path('.') +p = Path(".") # use pattern "*.*" to exclude directories -for entry in p.glob('*'): - print('File:' if entry.is_file() else 'Folder:', entry) +for entry in p.glob("*"): + print("File:" if entry.is_file() else "Folder:", entry) """ diff --git a/ch08/files/manipulation.py b/ch08/files/manipulation.py index 890507c..0c4169a 100644 --- a/ch08/files/manipulation.py +++ b/ch08/files/manipulation.py @@ -3,29 +3,29 @@ from string import ascii_letters -chars = ascii_letters + ' ' +chars = ascii_letters + " " def sanitize(s, chars): - return ''.join(c for c in s if c in chars) + return "".join(c for c in s if c in chars) def reverse(s): return s[::-1] -with open('fear.txt') as stream: +with open("fear.txt") as stream: lines = [line.rstrip() for line in stream] # let's write the mirrored version of the file -with open('raef.txt', 'w') as stream: - stream.write('\n'.join(reverse(line) for line in lines)) +with open("raef.txt", "w") as stream: + stream.write("\n".join(reverse(line) for line in lines)) # now we can calculate some statistics lines = [sanitize(line, chars) for line in lines] -whole = ' '.join(lines) +whole = " ".join(lines) # we perform comparisons on the lowercased version of `whole` diff --git a/ch08/files/open_try.py b/ch08/files/open_try.py index 98a3137..c56b688 100644 --- a/ch08/files/open_try.py +++ b/ch08/files/open_try.py @@ -1,5 +1,5 @@ # files/open_try.py -fh = open('fear.txt', 'rt') # r: read, t: text +fh = open("fear.txt", "rt") # r: read, t: text for line in fh.readlines(): print(line.strip()) # remove whitespace and print @@ -8,7 +8,7 @@ # secured by try/finally -fh = open('fear.txt', 'rt') +fh = open("fear.txt", "rt") try: for line in fh.readlines(): @@ -19,7 +19,7 @@ # equivalent to: -fh = open('fear.txt') # rt is default +fh = open("fear.txt") # rt is default try: for line in fh: # we can iterate directly on fh diff --git a/ch08/files/open_with.py b/ch08/files/open_with.py index 5dfb7de..73d8959 100644 --- a/ch08/files/open_with.py +++ b/ch08/files/open_with.py @@ -1,4 +1,4 @@ # files/open_with.py -with open('fear.txt') as fh: +with open("fear.txt") as fh: for line in fh: print(line.strip()) diff --git a/ch08/files/ops_create.py b/ch08/files/ops_create.py index 69d98c1..1697244 100644 --- a/ch08/files/ops_create.py +++ b/ch08/files/ops_create.py @@ -3,7 +3,7 @@ from pathlib import Path -base_path = Path('ops_example') +base_path = Path("ops_example") # let's perform an initial cleanup just in case if base_path.exists() and base_path.is_dir(): @@ -12,24 +12,24 @@ # now we create the directory base_path.mkdir() -path_b = base_path / 'A' / 'B' -path_c = base_path / 'A' / 'C' -path_d = base_path / 'A' / 'D' +path_b = base_path / "A" / "B" +path_c = base_path / "A" / "C" +path_d = base_path / "A" / "D" path_b.mkdir(parents=True) path_c.mkdir() # no need for parents now, as 'A' has been created # we add three files in `ops_example/A/B` -for filename in ('ex1.txt', 'ex2.txt', 'ex3.txt'): - with open(path_b / filename, 'w') as stream: - stream.write(f'Some content here in {filename}\n') +for filename in ("ex1.txt", "ex2.txt", "ex3.txt"): + with open(path_b / filename, "w") as stream: + stream.write(f"Some content here in {filename}\n") shutil.move(path_b, path_d) # we can also rename files -ex1 = path_d / 'ex1.txt' -ex1.rename(ex1.parent / 'ex1.renamed.txt') +ex1 = path_d / "ex1.txt" +ex1.rename(ex1.parent / "ex1.renamed.txt") # now call $ tree ops_example diff --git a/ch08/files/paths.py b/ch08/files/paths.py index f40bb6a..9352755 100644 --- a/ch08/files/paths.py +++ b/ch08/files/paths.py @@ -2,7 +2,7 @@ from pathlib import Path -p = Path('fear.txt') +p = Path("fear.txt") print(p.absolute()) print(p.name) @@ -12,7 +12,7 @@ print(p.parts) print(p.absolute().parts) -readme_path = p.parent / '..' / '..' / 'README.rst' +readme_path = p.parent / ".." / ".." / "README.rst" print(readme_path.absolute()) print(readme_path.resolve()) diff --git a/ch08/files/print_file.py b/ch08/files/print_file.py index 34715a5..f2b9489 100644 --- a/ch08/files/print_file.py +++ b/ch08/files/print_file.py @@ -1,3 +1,3 @@ # files/print_file.py -with open('print_example.txt', 'w') as fw: - print('Hey I am printing into a file!!!', file=fw) +with open("print_example.txt", "w") as fw: + print("Hey I am printing into a file!!!", file=fw) diff --git a/ch08/files/read_write.py b/ch08/files/read_write.py index f41d891..5896b1b 100644 --- a/ch08/files/read_write.py +++ b/ch08/files/read_write.py @@ -1,7 +1,7 @@ # files/read_write.py -with open('fear.txt') as f: +with open("fear.txt") as f: lines = [line.rstrip() for line in f] -with open('fear_copy.txt', 'w') as fw: # w - write - fw.write('\n'.join(lines)) +with open("fear_copy.txt", "w") as fw: # w - write + fw.write("\n".join(lines)) diff --git a/ch08/files/read_write_bin.py b/ch08/files/read_write_bin.py index 1e238a6..4112ed9 100644 --- a/ch08/files/read_write_bin.py +++ b/ch08/files/read_write_bin.py @@ -1,7 +1,7 @@ # files/read_write_bin.py -with open('example.bin', 'wb') as fw: - fw.write(b'This is binary data...') +with open("example.bin", "wb") as fw: + fw.write(b"This is binary data...") -with open('example.bin', 'rb') as f: +with open("example.bin", "rb") as f: print(f.read()) # prints: b'This is binary data...' diff --git a/ch08/files/tmp.py b/ch08/files/tmp.py index f1dc231..95dcebe 100644 --- a/ch08/files/tmp.py +++ b/ch08/files/tmp.py @@ -2,8 +2,8 @@ from tempfile import NamedTemporaryFile, TemporaryDirectory -with TemporaryDirectory(dir='.') as td: - print('Temp directory:', td) +with TemporaryDirectory(dir=".") as td: + print("Temp directory:", td) with NamedTemporaryFile(dir=td) as t: name = t.name print(name) diff --git a/ch08/files/walking.pathlib.py b/ch08/files/walking.pathlib.py index 127dfd8..4110879 100644 --- a/ch08/files/walking.pathlib.py +++ b/ch08/files/walking.pathlib.py @@ -2,10 +2,10 @@ from pathlib import Path -p = Path('.') +p = Path(".") -for entry in p.rglob('*'): - print('File:' if entry.is_file() else 'Folder:', entry) +for entry in p.rglob("*"): + print("File:" if entry.is_file() else "Folder:", entry) """ diff --git a/ch08/files/walking.py b/ch08/files/walking.py index 4b76ca2..bd1d916 100644 --- a/ch08/files/walking.py +++ b/ch08/files/walking.py @@ -2,18 +2,18 @@ import os -for root, dirs, files in os.walk('.'): +for root, dirs, files in os.walk("."): abs_root = os.path.abspath(root) print(abs_root) if dirs: - print('Directories:') + print("Directories:") for dir_ in dirs: print(dir_) print() if files: - print('Files:') + print("Files:") for filename in files: print(filename) print() diff --git a/ch08/files/write_not_exists.py b/ch08/files/write_not_exists.py index be14982..3beeb1c 100644 --- a/ch08/files/write_not_exists.py +++ b/ch08/files/write_not_exists.py @@ -1,7 +1,13 @@ # files/write_not_exists.py -with open('write_x.txt', 'x') as fw: # this succeeds - fw.write('Writing line 1') +import pathlib +file_to_rem = pathlib.Path("write_x.txt") +file_to_rem.unlink() -with open('write_x.txt', 'x') as fw: # this fails - fw.write('Writing line 2') + +with open("write_x.txt", "x") as fw: # this succeeds + fw.write("Writing line 1") + + +# with open('write_x.txt', 'x') as fw: # this fails +# fw.write('Writing line 2') diff --git a/ch08/io_examples/string_io.py b/ch08/io_examples/string_io.py index 7f18f67..088f973 100644 --- a/ch08/io_examples/string_io.py +++ b/ch08/io_examples/string_io.py @@ -3,8 +3,8 @@ stream = io.StringIO() -stream.write('Learning Python Programming.\n') -print('Become a Python ninja!', file=stream) +stream.write("Learning Python Programming.\n") +print("Become a Python ninja!", file=stream) contents = stream.getvalue() print(contents) @@ -14,8 +14,8 @@ # better alternative, using a context manager with io.StringIO() as stream: - stream.write('Learning Python Programming.\n') - print('Become a Python ninja!', file=stream) + stream.write("Learning Python Programming.\n") + print("Become a Python ninja!", file=stream) contents = stream.getvalue() print(contents) diff --git a/ch08/json_examples/json_basic.py b/ch08/json_examples/json_basic.py index 1d777e9..9b48c0e 100644 --- a/ch08/json_examples/json_basic.py +++ b/ch08/json_examples/json_basic.py @@ -4,9 +4,9 @@ data = { - 'big_number': 2 ** 3141, - 'max_float': sys.float_info.max, - 'a_list': [2, 3, 5, 7], + "big_number": 2**3141, + "max_float": sys.float_info.max, + "a_list": [2, 3, 5, 7], } @@ -19,13 +19,13 @@ # let's see how passing indent affects dumps. info = { - 'full_name': 'Sherlock Holmes', - 'address': { - 'street': '221B Baker St', - 'zip': 'NW1 6XE', - 'city': 'London', - 'country': 'UK', - } + "full_name": "Sherlock Holmes", + "address": { + "street": "221B Baker St", + "zip": "NW1 6XE", + "city": "London", + "country": "UK", + }, } diff --git a/ch08/json_examples/json_cplx.py b/ch08/json_examples/json_cplx.py index 0a4b29c..160ac0b 100644 --- a/ch08/json_examples/json_cplx.py +++ b/ch08/json_examples/json_cplx.py @@ -7,16 +7,16 @@ def default(self, obj): print(f"ComplexEncoder.default: {obj=}") if isinstance(obj, complex): return { - '_meta': '_complex', - 'num': [obj.real, obj.imag], + "_meta": "_complex", + "num": [obj.real, obj.imag], } return super().default(obj) data = { - 'an_int': 42, - 'a_float': 3.14159265, - 'a_complex': 3 + 4j, + "an_int": 42, + "a_float": 3.14159265, + "a_complex": 3 + 4j, } json_data = json.dumps(data, cls=ComplexEncoder) @@ -27,8 +27,8 @@ def default(self, obj): def object_hook(obj): print(f"object_hook: {obj=}") try: - if obj['_meta'] == '_complex': - return complex(*obj['num']) + if obj["_meta"] == "_complex": + return complex(*obj["num"]) except KeyError: return obj diff --git a/ch08/json_examples/json_datetime.py b/ch08/json_examples/json_datetime.py index d9e9899..8deee62 100644 --- a/ch08/json_examples/json_datetime.py +++ b/ch08/json_examples/json_datetime.py @@ -2,6 +2,7 @@ # exercise: do the same for date import json from datetime import datetime, timedelta, timezone +from pprint import pprint now = datetime.now() @@ -17,18 +18,18 @@ def default(self, obj): off = None return { - '_meta': '_datetime', - 'data': obj.timetuple()[:6] + (obj.microsecond, ), - 'utcoffset': off, + "_meta": "_datetime", + "data": obj.timetuple()[:6] + (obj.microsecond,), + "utcoffset": off, } return super().default(obj) data = { - 'an_int': 42, - 'a_float': 3.14159265, - 'a_datetime': now, - 'a_datetime_tz': now_tz, + "an_int": 42, + "a_float": 3.14159265, + "a_datetime": now, + "a_datetime_tz": now_tz, } json_data = json.dumps(data, cls=DatetimeEncoder) @@ -38,20 +39,19 @@ def default(self, obj): def object_hook(obj): try: - if obj['_meta'] == '_datetime': - if obj['utcoffset'] is None: + if obj["_meta"] == "_datetime": + if obj["utcoffset"] is None: tz = None else: - tz = timezone(timedelta(seconds=obj['utcoffset'])) - return datetime(*obj['data'], tzinfo=tz) + tz = timezone(timedelta(seconds=obj["utcoffset"])) + return datetime(*obj["data"], tzinfo=tz) except KeyError: return obj data_out = json.loads(json_data, object_hook=object_hook) -from pprint import pprint pprint(data_out, indent=2) print(data_out) -assert data_out['a_datetime'] == data['a_datetime'] -assert data_out['a_datetime_tz'] == data['a_datetime_tz'] +assert data_out["a_datetime"] == data["a_datetime"] +assert data_out["a_datetime_tz"] == data["a_datetime_tz"] diff --git a/ch08/json_examples/json_tuple.py b/ch08/json_examples/json_tuple.py index 277beef..01c1759 100644 --- a/ch08/json_examples/json_tuple.py +++ b/ch08/json_examples/json_tuple.py @@ -3,7 +3,7 @@ data_in = { - 'a_tuple': (1, 2, 3, 4, 5), + "a_tuple": (1, 2, 3, 4, 5), } diff --git a/ch08/persistence/alchemy.py b/ch08/persistence/alchemy.py index bcf7ce8..01690ca 100644 --- a/ch08/persistence/alchemy.py +++ b/ch08/persistence/alchemy.py @@ -8,19 +8,19 @@ # Create a couple of people -anakin = Person(name='Anakin Skywalker', age=32) -obi1 = Person(name='Obi-Wan Kenobi', age=40) +anakin = Person(name="Anakin Skywalker", age=32) +obi1 = Person(name="Obi-Wan Kenobi", age=40) # Add email addresses for both of them obi1.addresses = [ - Address(email='obi1@example.com'), - Address(email='wanwan@example.com'), + Address(email="obi1@example.com"), + Address(email="wanwan@example.com"), ] # another way: we can simply append -anakin.addresses.append(Address(email='ani@example.com')) -anakin.addresses.append(Address(email='evil.dart@example.com')) -anakin.addresses.append(Address(email='vader@example.com')) +anakin.addresses.append(Address(email="ani@example.com")) +anakin.addresses.append(Address(email="evil.dart@example.com")) +anakin.addresses.append(Address(email="vader@example.com")) # Add people to the session. This adds addresses too. session.add(anakin) @@ -29,14 +29,10 @@ # Query and display both -obi1 = session.query(Person).filter( - Person.name.like('Obi%') -).first() +obi1 = session.query(Person).filter(Person.name.like("Obi%")).first() print(obi1, obi1.addresses) -anakin = session.query(Person).filter( - Person.name=='Anakin Skywalker' -).first() +anakin = session.query(Person).filter(Person.name == "Anakin Skywalker").first() print(anakin, anakin.addresses) # capture anakin.id @@ -52,12 +48,13 @@ def display_info(): # display results for address in addresses: - print(f'{address.person.name} <{address.email}>') + print(f"{address.person.name} <{address.email}>") # display how many objects we have in total - print('people: {}, addresses: {}'.format( - session.query(Person).count(), - session.query(Address).count()) + print( + "people: {}, addresses: {}".format( + session.query(Person).count(), session.query(Address).count() + ) ) diff --git a/ch08/persistence/alchemy_models.py b/ch08/persistence/alchemy_models.py index 2caf863..e2ef91c 100644 --- a/ch08/persistence/alchemy_models.py +++ b/ch08/persistence/alchemy_models.py @@ -1,46 +1,46 @@ # persistence/alchemy_models.py from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import ( - Column, Integer, String, ForeignKey, create_engine) +from sqlalchemy import Column, Integer, String, ForeignKey, create_engine from sqlalchemy.orm import relationship # swap these lines to work with an actual DB file # engine = create_engine('sqlite:///example.db') -engine = create_engine('sqlite:///:memory:') +engine = create_engine("sqlite:///:memory:") Base = declarative_base() class Person(Base): - __tablename__ = 'person' + __tablename__ = "person" id = Column(Integer, primary_key=True) name = Column(String) age = Column(Integer) addresses = relationship( - 'Address', - back_populates='person', - order_by='Address.email', - cascade='all, delete-orphan' + "Address", + back_populates="person", + order_by="Address.email", + cascade="all, delete-orphan", ) def __repr__(self): - return f'{self.name}(id={self.id})' + return f"{self.name}(id={self.id})" class Address(Base): - __tablename__ = 'address' + __tablename__ = "address" id = Column(Integer, primary_key=True) email = Column(String) - person_id = Column(ForeignKey('person.id')) - person = relationship('Person', back_populates='addresses') + person_id = Column(ForeignKey("person.id")) + person = relationship("Person", back_populates="addresses") def __str__(self): return self.email + __repr__ = __str__ diff --git a/ch08/persistence/pickler.py b/ch08/persistence/pickler.py index 53804d5..305b6cf 100644 --- a/ch08/persistence/pickler.py +++ b/ch08/persistence/pickler.py @@ -10,23 +10,22 @@ class Person: id: int def greet(self): - print(f'Hi, I am {self.first_name} {self.last_name}' - f' and my ID is {self.id}') + print(f"Hi, I am {self.first_name} {self.last_name}" f" and my ID is {self.id}") people = [ - Person('Obi-Wan', 'Kenobi', 123), - Person('Anakin', 'Skywalker', 456), + Person("Obi-Wan", "Kenobi", 123), + Person("Anakin", "Skywalker", 456), ] # save data in binary format to a file -with open('data.pickle', 'wb') as stream: +with open("data.pickle", "wb") as stream: pickle.dump(people, stream) # load data from a file -with open('data.pickle', 'rb') as stream: +with open("data.pickle", "rb") as stream: peeps = pickle.load(stream) diff --git a/ch08/persistence/shelf.py b/ch08/persistence/shelf.py index 21da4f0..00900fc 100644 --- a/ch08/persistence/shelf.py +++ b/ch08/persistence/shelf.py @@ -8,33 +8,33 @@ def __init__(self, name, id): self.id = id -with shelve.open('shelf1.shelve') as db: - db['obi1'] = Person('Obi-Wan', 123) - db['ani'] = Person('Anakin', 456) - db['a_list'] = [2, 3, 5] - db['delete_me'] = 'we will have to delete this one...' +with shelve.open("shelf1.shelve") as db: + db["obi1"] = Person("Obi-Wan", 123) + db["ani"] = Person("Anakin", 456) + db["a_list"] = [2, 3, 5] + db["delete_me"] = "we will have to delete this one..." print(list(db.keys())) # ['ani', 'delete_me', 'a_list', 'obi1'] - del db['delete_me'] # gone! + del db["delete_me"] # gone! print(list(db.keys())) # ['ani', 'a_list', 'obi1'] - print('delete_me' in db) # False - print('ani' in db) # True + print("delete_me" in db) # False + print("ani" in db) # True - a_list = db['a_list'] + a_list = db["a_list"] a_list.append(7) - db['a_list'] = a_list + db["a_list"] = a_list - print(db['a_list']) # [2, 3, 5, 7] + print(db["a_list"]) # [2, 3, 5, 7] # this way allows writeback: # working with lists is easier, but consumes more memory and # closing the file takes longer. -with shelve.open('shelf2.shelve', writeback=True) as db: - db['a_list'] = [11, 13, 17] - db['a_list'].append(19) # in-place append! +with shelve.open("shelf2.shelve", writeback=True) as db: + db["a_list"] = [11, 13, 17] + db["a_list"].append(19) # in-place append! - print(db['a_list']) # [11, 13, 17, 19] + print(db["a_list"]) # [11, 13, 17, 19] diff --git a/ch08/requirements/requirements.in b/ch08/requirements/requirements.in deleted file mode 100644 index d527bb1..0000000 --- a/ch08/requirements/requirements.in +++ /dev/null @@ -1,2 +0,0 @@ -requests~=2.25.1 -sqlalchemy~=1.4.15 diff --git a/ch08/requirements/requirements.txt b/ch08/requirements/requirements.txt deleted file mode 100644 index c0d43b8..0000000 --- a/ch08/requirements/requirements.txt +++ /dev/null @@ -1,20 +0,0 @@ -# -# This file is autogenerated by pip-compile -# To update, run: -# -# pip-compile requirements.in -# -certifi==2020.12.5 - # via requests -chardet==4.0.0 - # via requests -greenlet==1.1.0 - # via sqlalchemy -idna==2.10 - # via requests -requests==2.25.1 - # via -r requirements.in -sqlalchemy==1.4.15 - # via -r requirements.in -urllib3==1.26.4 - # via requests diff --git a/ch09/hlib.py b/ch09/hlib.py index 88b04c5..0ea2bab 100644 --- a/ch09/hlib.py +++ b/ch09/hlib.py @@ -1,54 +1,34 @@ # hlib.py -# NOT A PYTHON MODULE - DO NOT ATTEMPT TO RUN # hlib.py ->>> import hashlib ->>> hashlib.algorithms_available -{'mdc2', 'sha224', 'whirlpool', 'sha1', 'sha3_512', 'sha512_256', - 'sha256', 'md4', 'sha384', 'blake2s', 'sha3_224', 'sha3_384', - 'shake_256', 'blake2b', 'ripemd160', 'sha512', 'md5-sha1', - 'shake_128', 'sha3_256', 'sha512_224', 'md5', 'sm3'} ->>> hashlib.algorithms_guaranteed -{'blake2s', 'md5', 'sha224', 'sha3_512', 'shake_256', 'sha3_256', - 'shake_128', 'sha256', 'sha1', 'sha512', 'blake2b', 'sha3_384', - 'sha384', 'sha3_224'} - ->>> h = hashlib.blake2b() ->>> h.update(b'Hash me') ->>> h.update(b' now!') ->>> h.hexdigest() -'56441b566db9aafcf8cdad3a4729fa4b2bfaab0ada36155ece29f52ff70e1e9d' -'7f54cacfe44bc97c7e904cf79944357d023877929430bc58eb2dae168e73cedf' ->>> h.digest() -b'VD\x1bVm\xb9\xaa\xfc\xf8\xcd\xad:G)\xfaK+\xfa\xab\n\xda6\x15^' -b'\xce)\xf5/\xf7\x0e\x1e\x9d\x7fT\xca\xcf\xe4K\xc9|~\x90L\xf7' -b'\x99D5}\x028w\x92\x940\xbcX\xeb-\xae\x16\x8es\xce\xdf' ->>> h.block_size -128 ->>> h.digest_size -64 ->>> h.name -'blake2b' - ->>> hashlib.sha256(b'Hash me now!').hexdigest() -'10d561fa94a89a25ea0c7aa47708bdb353bbb062a17820292cd905a3a60d6783' - - ->>> import hashlib ->>> h1 = hashlib.blake2b(b'Important data', digest_size=16, -... person=b'part-1') ->>> h2 = hashlib.blake2b(b'Important data', digest_size=16, -... person=b'part-2') ->>> h3 = hashlib.blake2b(b'Important data', digest_size=16) ->>> h1.hexdigest() -'c06b9af95d5aa6307e7e3fd025a15646' ->>> h2.hexdigest() -'9cb03be8f3114d0f06bddaedce2079c4' ->>> h3.hexdigest() -'7d35308ca3b042b5184728d2b1283d0d' - ->>> import os ->>> dk = hashlib.pbkdf2_hmac('sha256', b'Password123', -... salt=os.urandom(16), iterations=100000) ->>> dk.hex() -'f8715c37906df067466ce84973e6e52a955be025a59c9100d9183c4cbec27a9e' +import hashlib +import os + +print(hashlib.algorithms_available) +print(hashlib.algorithms_guaranteed) + +h = hashlib.blake2b() +h.update(b"Hash me") +h.update(b" now!") + +print(h.hexdigest()) +print(h.digest()) + +print(h.block_size) +print(h.digest_size) +print(h.name) + +print(hashlib.sha256(b"Hash me now!").hexdigest()) + +h1 = hashlib.blake2b(b"Important data", digest_size=16, person=b"part-1") +h2 = hashlib.blake2b(b"Important data", digest_size=16, person=b"part-2") + +h3 = hashlib.blake2b(b"Important data", digest_size=16) +print(h1.hexdigest()) +print(h2.hexdigest()) +print(h3.hexdigest()) + +dk = hashlib.pbkdf2_hmac( + "sha256", b"Password123", salt=os.urandom(16), iterations=100000 +) +print(dk.hex()) diff --git a/ch09/hmc.py b/ch09/hmc.py index 4988683..2f22595 100644 --- a/ch09/hmc.py +++ b/ch09/hmc.py @@ -4,12 +4,12 @@ def calc_digest(key, message): - key = bytes(key, 'utf-8') - message = bytes(message, 'utf-8') + key = bytes(key, "utf-8") + message = bytes(message, "utf-8") dig = hmac.new(key, message, hashlib.sha256) return dig.hexdigest() -mac = calc_digest('secret-key', 'Important Message') +mac = calc_digest("secret-key", "Important Message") print(mac) diff --git a/ch09/jwt/claims_auth.py b/ch09/jwt/claims_auth.py index a5b0fc3..734fd96 100644 --- a/ch09/jwt/claims_auth.py +++ b/ch09/jwt/claims_auth.py @@ -2,20 +2,21 @@ import jwt -data = {'payload': 'data', 'iss': 'hein', 'aud': 'learn-python'} +data = {"payload": "data", "iss": "hein", "aud": "learn-python"} -secret = 'secret-key' +secret = "secret-key" token = jwt.encode(data, secret) def decode(token, secret, issuer=None, audience=None): try: - print(jwt.decode(token, secret, issuer=issuer, - audience=audience, algorithms=["HS256"])) - except ( - jwt.InvalidIssuerError, jwt.InvalidAudienceError - ) as err: + print( + jwt.decode( + token, secret, issuer=issuer, audience=audience, algorithms=["HS256"] + ) + ) + except (jwt.InvalidIssuerError, jwt.InvalidAudienceError) as err: print(err) print(type(err)) @@ -23,16 +24,16 @@ def decode(token, secret, issuer=None, audience=None): decode(token, secret) # not providing the issuer won't break -decode(token, secret, audience='learn-python') +decode(token, secret, audience="learn-python") # not providing the audience will break -decode(token, secret, issuer='hein') +decode(token, secret, issuer="hein") # both will break -decode(token, secret, issuer='wrong', audience='learn-python') -decode(token, secret, issuer='hein', audience='wrong') +decode(token, secret, issuer="wrong", audience="learn-python") +decode(token, secret, issuer="hein", audience="wrong") -decode(token, secret, issuer='hein', audience='learn-python') +decode(token, secret, issuer="hein", audience="learn-python") """ diff --git a/ch09/jwt/claims_time.py b/ch09/jwt/claims_time.py index 73fafaf..d941761 100644 --- a/ch09/jwt/claims_time.py +++ b/ch09/jwt/claims_time.py @@ -10,21 +10,19 @@ exp = iat + timedelta(seconds=3) -data = {'payload': 'data', 'nbf': nfb, 'exp': exp, 'iat': iat} +data = {"payload": "data", "nbf": nfb, "exp": exp, "iat": iat} def decode(token, secret): print(time()) try: - print(jwt.decode(token, secret, algorithms=['HS256'])) - except ( - jwt.ImmatureSignatureError, jwt.ExpiredSignatureError - ) as err: + print(jwt.decode(token, secret, algorithms=["HS256"])) + except (jwt.ImmatureSignatureError, jwt.ExpiredSignatureError) as err: print(err) print(type(err)) -secret = 'secret-key' +secret = "secret-key" token = jwt.encode(data, secret) diff --git a/ch09/jwt/tok.py b/ch09/jwt/tok.py index 03df9aa..eaf62fd 100644 --- a/ch09/jwt/tok.py +++ b/ch09/jwt/tok.py @@ -2,20 +2,20 @@ import jwt -data = {'payload': 'data', 'id': 123456789} -algs = ['HS256', 'HS512'] +data = {"payload": "data", "id": 123456789} +algs = ["HS256", "HS512"] -token = jwt.encode(data, 'secret-key') -data_out = jwt.decode(token, 'secret-key', algorithms=algs) +token = jwt.encode(data, "secret-key") +data_out = jwt.decode(token, "secret-key", algorithms=algs) print(token) print(data_out) # decode without verifying the signature -jwt.decode(token, options={'verify_signature': False}) +jwt.decode(token, options={"verify_signature": False}) # let's use another algorithm -token512 = jwt.encode(data, 'secret-key', algorithm='HS512') -data_out = jwt.decode(token512, 'secret-key', algorithms=['HS512']) +token512 = jwt.encode(data, "secret-key", algorithm="HS512") +data_out = jwt.decode(token512, "secret-key", algorithms=["HS512"]) print(data_out) diff --git a/ch09/jwt/token_rsa.py b/ch09/jwt/token_rsa.py index cfe3f31..2cace20 100644 --- a/ch09/jwt/token_rsa.py +++ b/ch09/jwt/token_rsa.py @@ -1,28 +1,27 @@ # jwt/token_rsa.py import jwt -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import serialization +# from cryptography.hazmat.backends import default_backend +# from cryptography.hazmat.primitives import serialization -data = {'payload': 'data'} +data = {"payload": "data"} -def encode(data, priv_filename, algorithm='RS256'): - with open(priv_filename, 'rb') as key: +def encode(data, priv_filename, algorithm="RS256"): + with open(priv_filename, "rb") as key: private_key = key.read() return jwt.encode(data, private_key, algorithm=algorithm) -def decode(data, pub_filename, algorithm='RS256'): - - with open(pub_filename, 'rb') as key: +def decode(data, pub_filename, algorithm="RS256"): + with open(pub_filename, "rb") as key: public_key = key.read() return jwt.decode(data, public_key, algorithms=[algorithm]) -token = encode(data, 'jwt/rsa/key') -data_out = decode(token, 'jwt/rsa/key.pub') +token = encode(data, "jwt/rsa/key") +data_out = decode(token, "jwt/rsa/key.pub") print(data_out) diff --git a/ch09/requirements/requirements.in b/ch09/requirements/requirements.in deleted file mode 100644 index b9bbb44..0000000 --- a/ch09/requirements/requirements.in +++ /dev/null @@ -1,2 +0,0 @@ -pyjwt -cryptography diff --git a/ch09/requirements/requirements.txt b/ch09/requirements/requirements.txt deleted file mode 100644 index 95f0f7a..0000000 --- a/ch09/requirements/requirements.txt +++ /dev/null @@ -1,14 +0,0 @@ -# -# This file is autogenerated by pip-compile with python 3.9 -# To update, run: -# -# pip-compile requirements.in -# -cffi==1.14.5 - # via cryptography -cryptography==3.4.7 - # via -r requirements.in -pycparser==2.20 - # via cffi -pyjwt==2.1.0 - # via -r requirements.in diff --git a/ch09/secrs/secr_gen.py b/ch09/secrs/secr_gen.py index 0b04278..5a37d05 100644 --- a/ch09/secrs/secr_gen.py +++ b/ch09/secrs/secr_gen.py @@ -5,18 +5,20 @@ def generate_pwd(length=8): chars = digits + ascii_letters - return ''.join(secrets.choice(chars) for c in range(length)) + return "".join(secrets.choice(chars) for c in range(length)) def generate_secure_pwd(length=16, upper=3, digits=3): if length < upper + digits + 1: - raise ValueError('Nice try!') + raise ValueError("Nice try!") while True: pwd = generate_pwd(length) - if (any(c.islower() for c in pwd) + if ( + any(c.islower() for c in pwd) and sum(c.isupper() for c in pwd) >= upper - and sum(c.isdigit() for c in pwd) >= digits): + and sum(c.isdigit() for c in pwd) >= digits + ): return pwd diff --git a/ch09/secrs/secr_rand.py b/ch09/secrs/secr_rand.py index 12eb575..4792e12 100644 --- a/ch09/secrs/secr_rand.py +++ b/ch09/secrs/secr_rand.py @@ -3,9 +3,9 @@ # utils -print(secrets.choice('Choose one of these words'.split())) +print(secrets.choice("Choose one of these words".split())) -print(secrets.randbelow(10 ** 6)) +print(secrets.randbelow(10**6)) print(secrets.randbits(32)) @@ -18,7 +18,7 @@ print(secrets.token_urlsafe(32)) # compare digests against timing attacks -secrets.compare_digest('abc123', 'abc123') +secrets.compare_digest("abc123", "abc123") """ $ python secr_rand.py diff --git a/ch09/secrs/secr_reset.py b/ch09/secrs/secr_reset.py index 40cde19..21cf859 100644 --- a/ch09/secrs/secr_reset.py +++ b/ch09/secrs/secr_reset.py @@ -4,7 +4,7 @@ def get_reset_pwd_url(https://github.com/token_length=16): token = secrets.token_urlsafe(token_length) - return f'https://example.com/reset-pwd/%7Btoken%7D' + return f"https://example.com/reset-pwd/%7Btoken%7D" print(get_reset_pwd_url()) diff --git a/ch10/api.py b/ch10/api.py index f07da57..5d609e5 100644 --- a/ch10/api.py +++ b/ch10/api.py @@ -8,13 +8,11 @@ class UserSchema(Schema): - """Represent a *valid* user. """ + """Represent a *valid* user.""" email = fields.Email(required=True) name = fields.Str(required=True, validate=Length(min=1)) - age = fields.Int( - required=True, validate=Range(min=18, max=65) - ) + age = fields.Int(required=True, validate=Range(min=18, max=65)) role = fields.Str() @pre_load() @@ -22,7 +20,7 @@ def strip_name(self, data, **kwargs): data_copy = deepcopy(data) try: - data_copy['name'] = data_copy['name'].strip() + data_copy["name"] = data_copy["name"].strip() except (AttributeError, KeyError, TypeError): pass @@ -46,12 +44,12 @@ def export(filename, users, overwrite=True): def get_valid_users(users): - """Yield one valid user at a time from users. """ + """Yield one valid user at a time from users.""" yield from filter(is_valid, users) def is_valid(user): - """Return whether or not the user is valid. """ + """Return whether or not the user is valid.""" return not schema.validate(user) @@ -60,9 +58,9 @@ def write_csv(filename, users): The users are assumed to be valid for the given CSV structure. """ - fieldnames = ['email', 'name', 'age', 'role'] + fieldnames = ["email", "name", "age", "role"] - with open(filename, 'w', newline='') as csvfile: + with open(filename, "w", newline="") as csvfile: writer = csv.DictWriter(csvfile, fieldnames=fieldnames) writer.writeheader() diff --git a/ch10/data.py b/ch10/data.py index 134bafd..65cf6e1 100644 --- a/ch10/data.py +++ b/ch10/data.py @@ -1,6 +1,13 @@ # data.py -def get_clean_data(source): +def load_data(source): + return source + + +def clean_data(data): + return data + +def get_clean_data(source): data = load_data(source) cleaned_data = clean_data(data) diff --git a/ch10/requirements/requirements.in b/ch10/requirements/requirements.in deleted file mode 100644 index 3984623..0000000 --- a/ch10/requirements/requirements.in +++ /dev/null @@ -1,2 +0,0 @@ -marshmallow -pytest diff --git a/ch10/requirements/requirements.txt b/ch10/requirements/requirements.txt deleted file mode 100644 index b886ab8..0000000 --- a/ch10/requirements/requirements.txt +++ /dev/null @@ -1,24 +0,0 @@ -# -# This file is autogenerated by pip-compile -# To update, run: -# -# pip-compile requirements.in -# -attrs==21.2.0 - # via pytest -iniconfig==1.1.1 - # via pytest -marshmallow==3.12.1 - # via -r requirements.in -packaging==20.9 - # via pytest -pluggy==0.13.1 - # via pytest -py==1.10.0 - # via pytest -pyparsing==2.4.7 - # via packaging -pytest==6.2.4 - # via -r requirements.in -toml==0.10.2 - # via pytest diff --git a/ch10/tests/test_api.py b/ch10/tests/test_api.py index a815d82..85cb7bc 100644 --- a/ch10/tests/test_api.py +++ b/ch10/tests/test_api.py @@ -9,37 +9,37 @@ @pytest.fixture def min_user(): - """Represent a valid user with minimal data. """ + """Represent a valid user with minimal data.""" return { - 'email': 'minimal@example.com', - 'name': 'Primus Minimus', - 'age': 18, + "email": "minimal@example.com", + "name": "Primus Minimus", + "age": 18, } @pytest.fixture def full_user(): - """Represent valid user with full data. """ + """Represent valid user with full data.""" return { - 'email': 'full@example.com', - 'name': 'Maximus Plenus', - 'age': 65, - 'role': 'emperor', + "email": "full@example.com", + "name": "Maximus Plenus", + "age": 65, + "role": "emperor", } @pytest.fixture def users(min_user, full_user): - """List of users, two valid and one invalid. """ + """List of users, two valid and one invalid.""" bad_user = { - 'email': 'invalid@example.com', - 'name': 'Horribilis', + "email": "invalid@example.com", + "name": "Horribilis", } return [min_user, bad_user, full_user] class TestIsValid: - """Test how code verifies whether a user is valid or not. """ + """Test how code verifies whether a user is valid or not.""" def test_minimal(self, min_user): assert is_valid(min_user) @@ -47,80 +47,77 @@ def test_minimal(self, min_user): def test_full(self, full_user): assert is_valid(full_user) - @pytest.mark.parametrize('age', range(18)) + @pytest.mark.parametrize("age", range(18)) def test_invalid_age_too_young(self, age, min_user): - min_user['age'] = age + min_user["age"] = age assert not is_valid(min_user) - @pytest.mark.parametrize('age', range(66, 100)) + @pytest.mark.parametrize("age", range(66, 100)) def test_invalid_age_too_old(self, age, min_user): - min_user['age'] = age + min_user["age"] = age assert not is_valid(min_user) - @pytest.mark.parametrize('age', ['NaN', 3.1415, None]) + @pytest.mark.parametrize("age", ["NaN", 3.1415, None]) def test_invalid_age_wrong_type(self, age, min_user): - min_user['age'] = age + min_user["age"] = age assert not is_valid(min_user) - @pytest.mark.parametrize('age', range(18, 66)) + @pytest.mark.parametrize("age", range(18, 66)) def test_valid_age(self, age, min_user): - min_user['age'] = age + min_user["age"] = age assert is_valid(min_user) - @pytest.mark.parametrize('field', ['email', 'name', 'age']) + @pytest.mark.parametrize("field", ["email", "name", "age"]) def test_mandatory_fields(self, field, min_user): del min_user[field] assert not is_valid(min_user) - @pytest.mark.parametrize('field', ['email', 'name', 'age']) + @pytest.mark.parametrize("field", ["email", "name", "age"]) def test_mandatory_fields_empty(self, field, min_user): - min_user[field] = '' + min_user[field] = "" assert not is_valid(min_user) def test_name_whitespace_only(self, min_user): - min_user['name'] = ' \n\t' + min_user["name"] = " \n\t" assert not is_valid(min_user) @pytest.mark.parametrize( - 'email, outcome', + "email, outcome", [ - ('missing_at.com', False), - ('@missing_start.com', False), - ('missing_end@', False), - ('missing_dot@example', False), - - ('good.one@example.com', True), - ('δοκιμή@παράδειγμα.δοκιμή', True), - ('аджай@экзампл.рус', True), - ] + ("missing_at.com", False), + ("@missing_start.com", False), + ("missing_end@", False), + ("missing_dot@example", False), + ("good.one@example.com", True), + ("δοκιμή@παράδειγμα.δοκιμή", True), + ("аджай@экзампл.рус", True), + ], ) def test_email(self, email, outcome, min_user): - min_user['email'] = email + min_user["email"] = email assert is_valid(min_user) == outcome @pytest.mark.parametrize( - 'field, value', + "field, value", [ - ('email', None), - ('email', 3.1415), - ('email', {}), - - ('name', None), - ('name', 3.1415), - ('name', {}), - - ('role', None), - ('role', 3.1415), - ('role', {}), - ] + ("email", None), + ("email", 3.1415), + ("email", {}), + ("name", None), + ("name", 3.1415), + ("name", {}), + ("role", None), + ("role", 3.1415), + ("role", {}), + ], ) def test_invalid_types(self, field, value, min_user): min_user[field] = value @@ -129,7 +126,7 @@ def test_invalid_types(self, field, value, min_user): class TestExport: - """Test behavior of `export` function. """ + """Test behavior of `export` function.""" @pytest.fixture def csv_file(self, tmp_path): @@ -142,9 +139,9 @@ def csv_file(self, tmp_path): @pytest.fixture def existing_file(self, tmp_path): - """Create a temporary file and put some content in it. """ - existing = tmp_path / 'existing.csv' - existing.write_text('Please leave me alone...') + """Create a temporary file and put some content in it.""" + existing = tmp_path / "existing.csv" + existing.write_text("Please leave me alone...") yield existing def test_export(self, users, csv_file): @@ -153,85 +150,74 @@ def test_export(self, users, csv_file): text = csv_file.read_text() assert ( - 'email,name,age,role\n' - 'minimal@example.com,Primus Minimus,18,\n' - 'full@example.com,Maximus Plenus,65,emperor\n' - ) == text + "email,name,age,role\n" + "minimal@example.com,Primus Minimus,18,\n" + "full@example.com,Maximus Plenus,65,emperor\n" + ) == text def test_export_quoting(self, min_user, csv_file): - min_user['name'] = 'A name, with a comma' + min_user["name"] = "A name, with a comma" export(csv_file, [min_user]) text = csv_file.read_text() assert ( - 'email,name,age,role\n' - 'minimal@example.com,"A name, with a comma",18,\n' - ) == text + "email,name,age,role\n" 'minimal@example.com,"A name, with a comma",18,\n' + ) == text def test_does_not_overwrite(self, users, existing_file): with pytest.raises(IOError) as err: export(existing_file, users, overwrite=False) - err.match( - r"'{}' already exists\.".format( - re.escape(str(existing_file)) - ) - ) + err.match(r"'{}' already exists\.".format(re.escape(str(existing_file)))) # let's also verify the file is still intact - assert existing_file.read_text() == ( - 'Please leave me alone...' - ) + assert existing_file.read_text() == ("Please leave me alone...") class TextExportMock: - """Example on how to test with mocks. """ + """Example on how to test with mocks.""" @pytest.fixture def write_csv_mock(self): - with patch('ch10.api.write_csv') as m: + with patch("ch10.api.write_csv") as m: yield m @pytest.fixture def get_valid_users_mock(self): - with patch('ch10.api.get_valid_users') as m: + with patch("ch10.api.get_valid_users") as m: yield m - def test_export( - self, write_csv_mock, get_valid_users_mock, users - ): - export('out.csv', users) + def test_export(self, write_csv_mock, get_valid_users_mock, users): + export("out.csv", users) # verify mocked funcs have been called properly assert [call(users)] == get_valid_users_mock.call_args_list valid_users = get_valid_users_mock.return_value - assert [ - call('out.csv', valid_users) - ] == write_csv_mock.call_args_list + assert [call("out.csv", valid_users)] == write_csv_mock.call_args_list class TestWriteCSV: - """Example on how to test with mocks. """ + """Example on how to test with mocks.""" @pytest.fixture def open_mock(self): - """Mocks the `open` function. """ - with patch('builtins.open', new_callable=mock_open()) as m: + """Mocks the `open` function.""" + with patch("builtins.open", new_callable=mock_open()) as m: yield m @pytest.fixture def csv_mock(self): - """Mocks the `csv` module as imported in `api.py`. """ - with patch('ch10.api.csv') as m: + """Mocks the `csv` module as imported in `api.py`.""" + with patch("ch10.api.csv") as m: yield m def test_write_csv(self, open_mock, csv_mock, users): - fieldnames = ['email', 'name', 'age', 'role'] + fieldnames = ["email", "name", "age", "role"] - write_csv('out.csv', users) + write_csv("out.csv", users) # verify both mocks are at work properly writer = csv_mock.DictWriter.return_value @@ -244,5 +230,7 @@ def test_write_csv(self, open_mock, csv_mock, users): assert [call()] == writer.writeheader.call_args_list assert [ - call(users[0]), call(users[1]), call(users[2]) + call(users[0]), + call(users[1]), + call(users[2]), ] == writer.writerow.call_args_list diff --git a/ch11/custom.py b/ch11/custom.py index dca00b7..11eb8b2 100644 --- a/ch11/custom.py +++ b/ch11/custom.py @@ -2,12 +2,12 @@ def debug(*msg, print_separator=True): print(*msg) if print_separator: - print('-' * 40) + print("-" * 40) -debug('Data is ...') -debug('Different', 'Strings', 'Are not a problem') -debug('After while loop', print_separator=False) +debug("Data is ...") +debug("Different", "Strings", "Are not a problem") +debug("After while loop", print_separator=False) """ diff --git a/ch11/custom_timestamp.py b/ch11/custom_timestamp.py index 46554f5..c732c04 100644 --- a/ch11/custom_timestamp.py +++ b/ch11/custom_timestamp.py @@ -5,21 +5,20 @@ def debug(*msg, timestamp=[None]): print(*msg) from time import time # local import + if timestamp[0] is None: - timestamp[0] = time() #1 + timestamp[0] = time() # 1 else: now = time() - print( - ' Time elapsed: {:.3f}s'.format(now - timestamp[0]) - ) - timestamp[0] = now #2 + print(" Time elapsed: {:.3f}s".format(now - timestamp[0])) + timestamp[0] = now # 2 -debug('Entering nasty piece of code...') -sleep(.3) -debug('First step done.') -sleep(.5) -debug('Second step done.') +debug("Entering nasty piece of code...") +sleep(0.3) +debug("First step done.") +sleep(0.5) +debug("Second step done.") """ diff --git a/ch11/log.py b/ch11/log.py index dabb7e9..fdce4ec 100644 --- a/ch11/log.py +++ b/ch11/log.py @@ -2,20 +2,19 @@ import logging logging.basicConfig( - filename='ch11.log', + filename="ch11.log", level=logging.DEBUG, - format='[%(asctime)s] %(levelname)s: %(message)s', - datefmt='%m/%d/%Y %I:%M:%S %p') + format="[%(asctime)s] %(levelname)s: %(message)s", + datefmt="%m/%d/%Y %I:%M:%S %p", +) mylist = [1, 2, 3] -logging.info('Starting to process `mylist`...') +logging.info("Starting to process `mylist`...") for position in range(4): try: - logging.debug( - 'Value at position %s is %s', position, mylist[position] - ) + logging.debug("Value at position %s is %s", position, mylist[position]) except IndexError: - logging.exception('Faulty position: %s', position) + logging.exception("Faulty position: %s", position) -logging.info('Done processing `mylist`.') +logging.info("Done processing `mylist`.") diff --git a/ch11/pdebugger.py b/ch11/pdebugger.py index 8088dd9..00cf4de 100644 --- a/ch11/pdebugger.py +++ b/ch11/pdebugger.py @@ -1,16 +1,18 @@ # pdebugger.py # d comes from a JSON payload we don't control -d = {'first': 'v1', 'second': 'v2', 'fourth': 'v4'} +d = {"first": "v1", "second": "v2", "fourth": "v4"} # keys also comes from a JSON payload we don't control -keys = ('first', 'second', 'third', 'fourth') +keys = ("first", "second", "third", "fourth") + def do_something_with_value(value): print(value) + for key in keys: do_something_with_value(d[key]) -print('Validation done.') +print("Validation done.") """ $ python pdebugger.py diff --git a/ch11/pdebugger_pdb.py b/ch11/pdebugger_pdb.py index e6c14ec..1040c61 100644 --- a/ch11/pdebugger_pdb.py +++ b/ch11/pdebugger_pdb.py @@ -1,14 +1,16 @@ # pdebugger_pdb.py +import pdb + # d comes from a JSON payload we don't control -d = {'first': 'v1', 'second': 'v2', 'fourth': 'v4'} +d = {"first": "v1", "second": "v2", "fourth": "v4"} # keys also comes from a JSON payload we don't control -keys = ('first', 'second', 'third', 'fourth') +keys = ("first", "second", "third", "fourth") + def do_something_with_value(value): print(value) -import pdb pdb.set_trace() # or: @@ -18,7 +20,7 @@ def do_something_with_value(value): for key in keys: do_something_with_value(d[key]) -print('Validation done.') +print("Validation done.") """ diff --git a/ch11/profiling/triples.py b/ch11/profiling/triples.py index 8809509..75d9467 100644 --- a/ch11/profiling/triples.py +++ b/ch11/profiling/triples.py @@ -1,4 +1,3 @@ - def calc_triples(mx): triples = [] for a in range(1, mx + 1): @@ -10,7 +9,7 @@ def calc_triples(mx): def calc_hypotenuse(a, b): - return (a**2 + b**2) ** .5 + return (a**2 + b**2) ** 0.5 def is_int(n): # n is expected to be a float @@ -31,7 +30,10 @@ def is_int(n): # n is expected to be a float 1 0.000 0.000 0.489 0.489 triples.py:3() 1 0.121 0.121 0.489 0.489 triples.py:3(calc_triples) 1 0.000 0.000 0.489 0.489 {built-in method builtins.exec} - 1034 0.000 0.000 0.000 0.000 {method 'append' of 'list' objects} - 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects} - 500500 0.021 0.000 0.021 0.000 {method 'is_integer' of 'float' objects} + 1034 0.000 0.000 0.000 0.000 {method 'append' of 'list' + objects} + 1 0.000 0.000 0.000 0.000 {method 'disable' of + '_lsprof.Profiler' objects} + 500500 0.021 0.000 0.021 0.000 {method 'is_integer' of 'float' + objects} """ diff --git a/ch11/profiling/triples_v2.py b/ch11/profiling/triples_v2.py index b0b3a15..a3b5a55 100644 --- a/ch11/profiling/triples_v2.py +++ b/ch11/profiling/triples_v2.py @@ -1,4 +1,3 @@ - def calc_triples(mx): triples = [] for a in range(1, mx + 1): @@ -10,7 +9,7 @@ def calc_triples(mx): def calc_hypotenuse(a, b): - return (a*a + b*b) ** .5 + return (a * a + b * b) ** 0.5 def is_int(n): # n is expected to be a float @@ -31,7 +30,10 @@ def is_int(n): # n is expected to be a float 1 0.000 0.000 0.288 0.288 triples_v2.py:3() 1 0.120 0.120 0.288 0.288 triples_v2.py:3(calc_triples) 1 0.000 0.000 0.288 0.288 {built-in method builtins.exec} - 1034 0.000 0.000 0.000 0.000 {method 'append' of 'list' objects} - 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects} - 500500 0.020 0.000 0.020 0.000 {method 'is_integer' of 'float' objects} + 1034 0.000 0.000 0.000 0.000 {method 'append' of 'list' + objects} + 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof. + Profiler' objects} + 500500 0.020 0.000 0.020 0.000 {method 'is_integer' of 'float' + objects} """ diff --git a/ch11/profiling/triples_v3.py b/ch11/profiling/triples_v3.py index d74adeb..5bb87e6 100644 --- a/ch11/profiling/triples_v3.py +++ b/ch11/profiling/triples_v3.py @@ -1,4 +1,3 @@ - def calc_triples(mx): triples = [] for a in range(1, mx + 1): @@ -10,7 +9,7 @@ def calc_triples(mx): def calc_hypotenuse(a, b): - return (a*a + b*b) ** .5 + return (a * a + b * b) ** 0.5 def is_int(n): @@ -31,6 +30,8 @@ def is_int(n): 1 0.000 0.000 0.269 0.269 triples_v3.py:3() 1 0.116 0.116 0.269 0.269 triples_v3.py:3(calc_triples) 1 0.000 0.000 0.269 0.269 {built-in method builtins.exec} - 1034 0.000 0.000 0.000 0.000 {method 'append' of 'list' objects} - 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects} + 1034 0.000 0.000 0.000 0.000 {method 'append' of 'list' + objects} + 1 0.000 0.000 0.000 0.000 {method 'disable' of + '_lsprof.Profiler' objects} """ diff --git a/ch11/requirements.in b/ch11/requirements.in deleted file mode 100644 index a226ff7..0000000 --- a/ch11/requirements.in +++ /dev/null @@ -1 +0,0 @@ -pdbpp diff --git a/ch11/requirements.txt b/ch11/requirements.txt deleted file mode 100644 index b4b079a..0000000 --- a/ch11/requirements.txt +++ /dev/null @@ -1,16 +0,0 @@ -# -# This file is autogenerated by pip-compile with python 3.9 -# To update, run: -# -# pip-compile requirements.in -# -fancycompleter==0.9.1 - # via pdbpp -pdbpp==0.10.3 - # via -r requirements.in -pygments==2.9.0 - # via pdbpp -pyrepl==0.9.0 - # via fancycompleter -wmctrl==0.4 - # via pdbpp diff --git a/ch12/guiscrape.py b/ch12/guiscrape.py index 733ec28..2df12e3 100644 --- a/ch12/guiscrape.py +++ b/ch12/guiscrape.py @@ -1,6 +1,5 @@ # guiscrape.py -from tkinter import * -from tkinter import ttk, filedialog, messagebox +from tkinter import Tk, ttk, filedialog, messagebox, StringVar, Listbox import base64 import json from pathlib import Path @@ -14,70 +13,70 @@ def fetch_url(): url = _url.get() - config['images'] = [] + config["images"] = [] _images.set(()) # initialised as an empty tuple try: page = requests.get(url) except requests.RequestException as err: sb(str(err)) else: - soup = BeautifulSoup(page.content, 'html.parser') + soup = BeautifulSoup(page.content, "html.parser") images = fetch_images(soup, url) if images: - _images.set(tuple(img['name'] for img in images)) - sb('Images found: {}'.format(len(images))) + _images.set(tuple(img["name"] for img in images)) + sb("Images found: {}".format(len(images))) else: - sb('No images found') - config['images'] = images + sb("No images found") + config["images"] = images def fetch_images(soup, base_url): images = [] - for img in soup.findAll('img'): - src = img.get('src') - img_url = f'{base_url}/{src}' - name = img_url.split('/')[-1] + for img in soup.findAll("img"): + src = img.get("src") + img_url = f"{base_url}/{src}" + name = img_url.split("/")[-1] images.append(dict(name=name, url=img_url)) return images def save(): - if not config.get('images'): - alert('No images to save') + if not config.get("images"): + alert("No images to save") return - if _save_method.get() == 'img': + if _save_method.get() == "img": dirname = filedialog.askdirectory(mustexist=True) save_images(dirname) else: filename = filedialog.asksaveasfilename( - initialfile='images.json', - filetypes=[('JSON', '.json')]) + initialfile="images.json", filetypes=[("JSON", ".json")] + ) save_json(filename) def save_images(dirname): - if dirname and config.get('images'): - for img in config['images']: - img_data = requests.get(img['url']).content - filename = Path(dirname).joinpath(img['name']) - with open(filename, 'wb') as f: + if dirname and config.get("images"): + for img in config["images"]: + img_data = requests.get(img["url"]).content + filename = Path(dirname).joinpath(img["name"]) + with open(filename, "wb") as f: f.write(img_data) - alert('Done') + alert("Done") def save_json(filename): - if filename and config.get('images'): + if filename and config.get("images"): data = {} - for img in config['images']: - img_data = requests.get(img['url']).content + for img in config["images"]: + img_data = requests.get(img["url"]).content b64_img_data = base64.b64encode(img_data) - str_img_data = b64_img_data.decode('utf-8') - data[img['name']] = str_img_data + str_img_data = b64_img_data.decode("utf-8") + data[img["name"]] = str_img_data - with open(filename, 'w') as ijson: + with open(filename, "w") as ijson: ijson.write(json.dumps(data)) - alert('Done') + alert("Done") def sb(msg): @@ -89,74 +88,62 @@ def alert(msg): if __name__ == "__main__": - _root = Tk() - _root.title('Scrape app') + _root.title("Scrape app") - _mainframe = ttk.Frame(_root, padding='5 5 5 5') + _mainframe = ttk.Frame(_root, padding="5 5 5 5") _mainframe.grid(row=0, column=0, sticky=(E, W, N, S)) - _url_frame = ttk.LabelFrame( - _mainframe, text='URL', padding='5 5 5 5') + _url_frame = ttk.LabelFrame(_mainframe, text="URL", padding="5 5 5 5") _url_frame.grid(row=0, column=0, sticky=(E, W)) _url_frame.columnconfigure(0, weight=1) _url_frame.rowconfigure(0, weight=1) _url = StringVar() - _url.set('http://localhost:8000/') - _url_entry = ttk.Entry( - _url_frame, width=40, textvariable=_url) + _url.set("http://localhost:8000/") + _url_entry = ttk.Entry(_url_frame, width=40, textvariable=_url) _url_entry.grid(row=0, column=0, sticky=(E, W, S, N), padx=5) - _fetch_btn = ttk.Button( - _url_frame, text='Fetch info', command=fetch_url) + _fetch_btn = ttk.Button(_url_frame, text="Fetch info", command=fetch_url) _fetch_btn.grid(row=0, column=1, sticky=W, padx=5) - _img_frame = ttk.LabelFrame( - _mainframe, text='Content', padding='9 0 0 0') + _img_frame = ttk.LabelFrame(_mainframe, text="Content", padding="9 0 0 0") _img_frame.grid(row=1, column=0, sticky=(N, S, E, W)) _images = StringVar() - _img_listbox = Listbox( - _img_frame, listvariable=_images, height=6, width=25) + _img_listbox = Listbox(_img_frame, listvariable=_images, height=6, width=25) _img_listbox.grid(row=0, column=0, sticky=(E, W), pady=5) - _scrollbar = ttk.Scrollbar( - _img_frame, orient=VERTICAL, command=_img_listbox.yview) + _scrollbar = ttk.Scrollbar(_img_frame, orient=VERTICAL, command=_img_listbox.yview) _scrollbar.grid(row=0, column=1, sticky=(S, N), pady=6) _img_listbox.configure(yscrollcommand=_scrollbar.set) _radio_frame = ttk.Frame(_img_frame) _radio_frame.grid(row=0, column=2, sticky=(N, S, W, E)) - _choice_lbl = ttk.Label( - _radio_frame, text="Choose how to save images") + _choice_lbl = ttk.Label(_radio_frame, text="Choose how to save images") _choice_lbl.grid(row=0, column=0, padx=5, pady=5) _save_method = StringVar() - _save_method.set('img') + _save_method.set("img") _img_only_radio = ttk.Radiobutton( - _radio_frame, text='As Images', variable=_save_method, - value='img') - _img_only_radio.grid( - row=1, column=0, padx=5, pady=2, sticky=W) - _img_only_radio.configure(state='normal') + _radio_frame, text="As Images", variable=_save_method, value="img" + ) + _img_only_radio.grid(row=1, column=0, padx=5, pady=2, sticky=W) + _img_only_radio.configure(state="normal") _json_radio = ttk.Radiobutton( - _radio_frame, text='As JSON', variable=_save_method, - value='json') + _radio_frame, text="As JSON", variable=_save_method, value="json" + ) _json_radio.grid(row=2, column=0, padx=5, pady=2, sticky=W) - _scrape_btn = ttk.Button( - _mainframe, text='Scrape!', command=save) + _scrape_btn = ttk.Button(_mainframe, text="Scrape!", command=save) _scrape_btn.grid(row=2, column=0, sticky=E, pady=5) - _status_frame = ttk.Frame( - _root, relief='sunken', padding='2 2 2 2') + _status_frame = ttk.Frame(_root, relief="sunken", padding="2 2 2 2") _status_frame.grid(row=1, column=0, sticky=(E, W, S)) _status_msg = StringVar() - _status_msg.set('Type a URL to start scraping...') - _status = ttk.Label( - _status_frame, textvariable=_status_msg, anchor=W) + _status_msg.set("Type a URL to start scraping...") + _status = ttk.Label(_status_frame, textvariable=_status_msg, anchor=W) _status.grid(row=0, column=0, sticky=(E, W)) _root.mainloop() diff --git a/ch12/requirements/requirements.in b/ch12/requirements/requirements.in deleted file mode 100644 index 1f3e778..0000000 --- a/ch12/requirements/requirements.in +++ /dev/null @@ -1,2 +0,0 @@ -beautifulsoup4 -requests diff --git a/ch12/requirements/requirements.txt b/ch12/requirements/requirements.txt deleted file mode 100644 index 1b76b71..0000000 --- a/ch12/requirements/requirements.txt +++ /dev/null @@ -1,20 +0,0 @@ -# -# This file is autogenerated by pip-compile with python 3.9 -# To update, run: -# -# pip-compile requirements.in -# -beautifulsoup4==4.9.3 - # via -r requirements.in -certifi==2021.5.30 - # via requests -charset-normalizer==2.0.3 - # via requests -idna==3.2 - # via requests -requests==2.26.0 - # via -r requirements.in -soupsieve==2.2.1 - # via beautifulsoup4 -urllib3==1.26.6 - # via requests diff --git a/ch12/scrape.py b/ch12/scrape.py index 5ee4630..2b69486 100644 --- a/ch12/scrape.py +++ b/ch12/scrape.py @@ -14,7 +14,7 @@ def scrape(url, format_, type_): except requests.RequestException as err: print(str(err)) else: - soup = BeautifulSoup(page.content, 'html.parser') + soup = BeautifulSoup(page.content, "html.parser") images = fetch_images(soup, url) images = filter_images(images, type_) save(images, format_) @@ -23,25 +23,22 @@ def scrape(url, format_, type_): def fetch_images(soup, base_url): # Works only with relative src paths. images = [] - for img in soup.findAll('img'): - src = img.get('src') - img_url = f'{base_url}/{src}' - name = img_url.split('/')[-1] + for img in soup.findAll("img"): + src = img.get("src") + img_url = f"{base_url}/{src}" + name = img_url.split("/")[-1] images.append(dict(name=name, url=img_url)) return images def filter_images(images, type_): - if type_ == 'all': + if type_ == "all": return images ext_map = { - 'png': ['.png'], - 'jpg': ['.jpg', '.jpeg'], + "png": [".png"], + "jpg": [".jpg", ".jpeg"], } - return [ - img for img in images - if matches_extension(img['name'], ext_map[type_]) - ] + return [img for img in images if matches_extension(img["name"], ext_map[type_])] def matches_extension(filename, extension_list): @@ -51,55 +48,53 @@ def matches_extension(filename, extension_list): def save(images, format_): if images: - if format_ == 'img': + if format_ == "img": save_images(images) else: save_json(images) - print('Done') + print("Done") else: - print('No images to save.') + print("No images to save.") def save_images(images): for img in images: - img_data = requests.get(img['url']).content - with open(img['name'], 'wb') as f: + img_data = requests.get(img["url"]).content + with open(img["name"], "wb") as f: f.write(img_data) def save_json(images): data = {} for img in images: - img_data = requests.get(img['url']).content + img_data = requests.get(img["url"]).content b64_img_data = base64.b64encode(img_data) - str_img_data = b64_img_data.decode('utf-8') - data[img['name']] = str_img_data + str_img_data = b64_img_data.decode("utf-8") + data[img["name"]] = str_img_data - with open('images.json', 'w') as ijson: + with open("images.json", "w") as ijson: ijson.write(json.dumps(data)) if __name__ == "__main__": - - parser = argparse.ArgumentParser( - description='Scrape a webpage.') - parser.add_argument( - '-t', - '--type', - choices=['all', 'png', 'jpg'], - default='all', - help='The image type we want to scrape.') - + parser = argparse.ArgumentParser(description="Scrape a webpage.") parser.add_argument( - '-f', - '--format', - choices=['img', 'json'], - default='img', - help='The format images are saved to.') + "-t", + "--type", + choices=["all", "png", "jpg"], + default="all", + help="The image type we want to scrape.", + ) parser.add_argument( - 'url', - help='The URL we want to scrape for images.') + "-f", + "--format", + choices=["img", "json"], + default="img", + help="The format images are saved to.", + ) + + parser.add_argument("url", help="The URL we want to scrape for images.") args = parser.parse_args() scrape(args.url, args.format, args.type) diff --git a/ch13/ch13-dataprep.ipynb b/ch13/ch13-dataprep.ipynb index 12baeb5..7036f83 100644 --- a/ch13/ch13-dataprep.ipynb +++ b/ch13/ch13-dataprep.ipynb @@ -17,7 +17,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -37,7 +37,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ @@ -47,7 +47,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ @@ -61,18 +61,18 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "['{\"username\": \"susan42\", \"name\": \"Emily Smith\", \"gender\": \"F\", \"email\": \"vmckinney@leon.com\", \"age\": 53, \"address\": \"66537 Riley Mission Apt. 337\\\\nNorth Jennifer, NH 95781\"}',\n", - " '{\"username\": \"sarahcarpenter\", \"name\": \"Michael Kane\", \"gender\": \"M\", \"email\": \"tamara51@yahoo.com\", \"age\": 58, \"address\": \"7129 Patrick Walks Suite 215\\\\nLaurenside, LA 97179\"}',\n", - " '{\"username\": \"kevin37\", \"name\": \"Nathaniel Miller\", \"gender\": \"M\", \"email\": \"maria21@gmail.com\", \"age\": 36, \"address\": \"8247 Manning Burgs Suite 806\\\\nLopezshire, MS 06606\"}']" + "['{\"username\": \"tracie73\", \"name\": \"David Maxwell\", \"gender\": \"M\", \"email\": \"rodriguezbobby@example.net\", \"age\": 41, \"address\": \"414 Arnold Street\\\\nRoyberg, IL 80169\"}',\n", + " '{\"username\": \"anna27\", \"name\": \"Colleen Peterson MD\", \"gender\": \"F\", \"email\": \"brandon94@example.com\", \"age\": 86, \"address\": \"29760 Chad Summit\\\\nWest Davidstad, PR 44505\"}',\n", + " '{\"username\": \"joshuamartin\", \"name\": \"Andrew Price\", \"gender\": \"M\", \"email\": \"sean59@example.com\", \"age\": 83, \"address\": \"845 Wiggins Greens\\\\nMandyview, ND 30513\"}']" ] }, - "execution_count": 4, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -110,7 +110,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -156,7 +156,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ @@ -179,7 +179,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 12, "metadata": {}, "outputs": [], "source": [ @@ -209,82 +209,42 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[{'user': '{\"username\": \"susan42\", \"name\": \"Emily Smith\", \"gender\": \"F\", \"email\": \"vmckinney@leon.com\", \"age\": 53, \"address\": \"66537 Riley Mission Apt. 337\\\\nNorth Jennifer, NH 95781\"}',\n", - " 'campaigns': [{'cmp_name': 'GRZ_20210131_20210411_30-40_F_GBP',\n", - " 'cmp_bgt': 253951,\n", - " 'cmp_spent': 17953,\n", - " 'cmp_clicks': 52573,\n", - " 'cmp_impr': 500001},\n", - " {'cmp_name': 'BYU_20210109_20221204_30-35_M_GBP',\n", - " 'cmp_bgt': 150314,\n", - " 'cmp_spent': 125884,\n", - " 'cmp_clicks': 24575,\n", - " 'cmp_impr': 499999},\n", - " {'cmp_name': 'GRZ_20211124_20220921_20-35_B_EUR',\n", - " 'cmp_bgt': 791397,\n", - " 'cmp_spent': 480963,\n", - " 'cmp_clicks': 39668,\n", - " 'cmp_impr': 499999},\n", - " {'cmp_name': 'GRZ_20210727_20220211_35-45_B_EUR',\n", - " 'cmp_bgt': 910204,\n", - " 'cmp_spent': 339997,\n", - " 'cmp_clicks': 16698,\n", - " 'cmp_impr': 500000},\n", - " {'cmp_name': 'BYU_20220216_20220407_20-25_F_EUR',\n", - " 'cmp_bgt': 393134,\n", - " 'cmp_spent': 158930,\n", - " 'cmp_clicks': 46631,\n", - " 'cmp_impr': 500000}]},\n", - " {'user': '{\"username\": \"sarahcarpenter\", \"name\": \"Michael Kane\", \"gender\": \"M\", \"email\": \"tamara51@yahoo.com\", \"age\": 58, \"address\": \"7129 Patrick Walks Suite 215\\\\nLaurenside, LA 97179\"}',\n", - " 'campaigns': [{'cmp_name': 'BYU_20220324_20221230_20-45_B_USD',\n", - " 'cmp_bgt': 819948,\n", - " 'cmp_spent': 105178,\n", - " 'cmp_clicks': 27755,\n", - " 'cmp_impr': 500004},\n", - " {'cmp_name': 'GRZ_20201008_20210604_30-40_B_GBP',\n", - " 'cmp_bgt': 829698,\n", - " 'cmp_spent': 143193,\n", - " 'cmp_clicks': 88114,\n", - " 'cmp_impr': 499998},\n", - " {'cmp_name': 'GRZ_20210710_20211130_25-30_B_USD',\n", - " 'cmp_bgt': 815470,\n", - " 'cmp_spent': 79377,\n", - " 'cmp_clicks': 28283,\n", - " 'cmp_impr': 500002},\n", - " {'cmp_name': 'AKX_20211028_20220112_25-35_F_USD',\n", - " 'cmp_bgt': 944028,\n", - " 'cmp_spent': 657427,\n", - " 'cmp_clicks': 6668,\n", - " 'cmp_impr': 499999},\n", - " {'cmp_name': 'AKX_20211025_20220314_25-35_M_EUR',\n", - " 'cmp_bgt': 39136,\n", - " 'cmp_spent': 29326,\n", - " 'cmp_clicks': 20927,\n", + "[{'user': '{\"username\": \"tracie73\", \"name\": \"David Maxwell\", \"gender\": \"M\", \"email\": \"rodriguezbobby@example.net\", \"age\": 41, \"address\": \"414 Arnold Street\\\\nRoyberg, IL 80169\"}',\n", + " 'campaigns': [{'cmp_name': 'KTR_20240725_20250515_45-50_F_EUR',\n", + " 'cmp_bgt': 178525,\n", + " 'cmp_spent': 116926,\n", + " 'cmp_clicks': 43985,\n", " 'cmp_impr': 499998},\n", - " {'cmp_name': 'BYU_20211227_20220615_20-35_F_USD',\n", - " 'cmp_bgt': 940412,\n", - " 'cmp_spent': 131757,\n", - " 'cmp_clicks': 57384,\n", - " 'cmp_impr': 500001},\n", - " {'cmp_name': 'AKX_20220323_20230602_35-55_M_GBP',\n", - " 'cmp_bgt': 545483,\n", - " 'cmp_spent': 96427,\n", - " 'cmp_clicks': 43290,\n", + " {'cmp_name': 'GRZ_20240325_20250718_30-40_F_GBP',\n", + " 'cmp_bgt': 897803,\n", + " 'cmp_spent': 745120,\n", + " 'cmp_clicks': 87866,\n", + " 'cmp_impr': 500000},\n", + " {'cmp_name': 'BYU_20230621_20231001_25-30_M_GBP',\n", + " 'cmp_bgt': 435973,\n", + " 'cmp_spent': 76953,\n", + " 'cmp_clicks': 39710,\n", + " 'cmp_impr': 500002}]},\n", + " {'user': '{\"username\": \"anna27\", \"name\": \"Colleen Peterson MD\", \"gender\": \"F\", \"email\": \"brandon94@example.com\", \"age\": 86, \"address\": \"29760 Chad Summit\\\\nWest Davidstad, PR 44505\"}',\n", + " 'campaigns': [{'cmp_name': 'AKX_20231224_20240209_45-50_B_GBP',\n", + " 'cmp_bgt': 604350,\n", + " 'cmp_spent': 40386,\n", + " 'cmp_clicks': 83080,\n", " 'cmp_impr': 499999},\n", - " {'cmp_name': 'AKX_20210917_20220912_35-55_B_USD',\n", - " 'cmp_bgt': 129347,\n", - " 'cmp_spent': 4747,\n", - " 'cmp_clicks': 88217,\n", - " 'cmp_impr': 499999}]}]" + " {'cmp_name': 'AKX_20230910_20240312_25-40_M_GBP',\n", + " 'cmp_bgt': 207699,\n", + " 'cmp_spent': 92627,\n", + " 'cmp_clicks': 35848,\n", + " 'cmp_impr': 500001}]}]" ] }, - "execution_count": 8, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -298,27 +258,27 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[{'cmp_name': 'GRZ_20210131_20210411_30-40_F_GBP',\n", - " 'cmp_bgt': 253951,\n", - " 'cmp_spent': 17953,\n", - " 'cmp_clicks': 52573,\n", - " 'cmp_impr': 500001,\n", - " 'user': '{\"username\": \"susan42\", \"name\": \"Emily Smith\", \"gender\": \"F\", \"email\": \"vmckinney@leon.com\", \"age\": 53, \"address\": \"66537 Riley Mission Apt. 337\\\\nNorth Jennifer, NH 95781\"}'},\n", - " {'cmp_name': 'BYU_20210109_20221204_30-35_M_GBP',\n", - " 'cmp_bgt': 150314,\n", - " 'cmp_spent': 125884,\n", - " 'cmp_clicks': 24575,\n", - " 'cmp_impr': 499999,\n", - " 'user': '{\"username\": \"susan42\", \"name\": \"Emily Smith\", \"gender\": \"F\", \"email\": \"vmckinney@leon.com\", \"age\": 53, \"address\": \"66537 Riley Mission Apt. 337\\\\nNorth Jennifer, NH 95781\"}'}]" + "[{'cmp_name': 'KTR_20240725_20250515_45-50_F_EUR',\n", + " 'cmp_bgt': 178525,\n", + " 'cmp_spent': 116926,\n", + " 'cmp_clicks': 43985,\n", + " 'cmp_impr': 499998,\n", + " 'user': '{\"username\": \"tracie73\", \"name\": \"David Maxwell\", \"gender\": \"M\", \"email\": \"rodriguezbobby@example.net\", \"age\": 41, \"address\": \"414 Arnold Street\\\\nRoyberg, IL 80169\"}'},\n", + " {'cmp_name': 'GRZ_20240325_20250718_30-40_F_GBP',\n", + " 'cmp_bgt': 897803,\n", + " 'cmp_spent': 745120,\n", + " 'cmp_clicks': 87866,\n", + " 'cmp_impr': 500000,\n", + " 'user': '{\"username\": \"tracie73\", \"name\": \"David Maxwell\", \"gender\": \"M\", \"email\": \"rodriguezbobby@example.net\", \"age\": 41, \"address\": \"414 Arnold Street\\\\nRoyberg, IL 80169\"}'}]" ] }, - "execution_count": 9, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } @@ -338,7 +298,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 15, "metadata": {}, "outputs": [], "source": [ @@ -364,7 +324,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.7" + "version": "3.11.2" } }, "nbformat": 4, diff --git a/ch13/ch13.ipynb b/ch13/ch13.ipynb index e95a9e5..8b3a820 100644 --- a/ch13/ch13.ipynb +++ b/ch13/ch13.ipynb @@ -17,7 +17,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -38,109 +38,9 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
cmp_namecmp_bgtcmp_spentcmp_clickscmp_impruser
0GRZ_20210131_20210411_30-40_F_GBP2539511795352573500001{\"username\": \"susan42\", \"name\": \"Emily Smith\",...
1BYU_20210109_20221204_30-35_M_GBP15031412588424575499999{\"username\": \"susan42\", \"name\": \"Emily Smith\",...
2GRZ_20211124_20220921_20-35_B_EUR79139748096339668499999{\"username\": \"susan42\", \"name\": \"Emily Smith\",...
3GRZ_20210727_20220211_35-45_B_EUR91020433999716698500000{\"username\": \"susan42\", \"name\": \"Emily Smith\",...
4BYU_20220216_20220407_20-25_F_EUR39313415893046631500000{\"username\": \"susan42\", \"name\": \"Emily Smith\",...
\n", - "
" - ], - "text/plain": [ - " cmp_name cmp_bgt cmp_spent cmp_clicks \\\n", - "0 GRZ_20210131_20210411_30-40_F_GBP 253951 17953 52573 \n", - "1 BYU_20210109_20221204_30-35_M_GBP 150314 125884 24575 \n", - "2 GRZ_20211124_20220921_20-35_B_EUR 791397 480963 39668 \n", - "3 GRZ_20210727_20220211_35-45_B_EUR 910204 339997 16698 \n", - "4 BYU_20220216_20220407_20-25_F_EUR 393134 158930 46631 \n", - "\n", - " cmp_impr user \n", - "0 500001 {\"username\": \"susan42\", \"name\": \"Emily Smith\",... \n", - "1 499999 {\"username\": \"susan42\", \"name\": \"Emily Smith\",... \n", - "2 499999 {\"username\": \"susan42\", \"name\": \"Emily Smith\",... \n", - "3 500000 {\"username\": \"susan42\", \"name\": \"Emily Smith\",... \n", - "4 500000 {\"username\": \"susan42\", \"name\": \"Emily Smith\",... " - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Load data from a json file into a DataFrame\n", "df = pd.read_json(\"data.json\")\n", @@ -152,26 +52,9 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "cmp_name 5140\n", - "cmp_bgt 5140\n", - "cmp_spent 5140\n", - "cmp_clicks 5140\n", - "cmp_impr 5140\n", - "user 5140\n", - "dtype: int64" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# OK! DataFrame is alive and well!\n", "# let's get a sense of how many rows there are and\n", @@ -181,201 +64,18 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
cmp_bgtcmp_spentcmp_clickscmp_impr
count5140.0000005140.0000005140.0000005140.000000
mean496331.855058249542.77821040414.236576499999.523346
std289001.241891219168.63640821704.1364802.010877
min1017.000000117.000000355.000000499991.000000
25%250725.50000070162.00000022865.250000499998.000000
50%495957.000000188704.00000037103.000000500000.000000
75%741076.500000381478.75000055836.000000500001.000000
max999860.000000984005.00000098912.000000500007.000000
\n", - "
" - ], - "text/plain": [ - " cmp_bgt cmp_spent cmp_clicks cmp_impr\n", - "count 5140.000000 5140.000000 5140.000000 5140.000000\n", - "mean 496331.855058 249542.778210 40414.236576 499999.523346\n", - "std 289001.241891 219168.636408 21704.136480 2.010877\n", - "min 1017.000000 117.000000 355.000000 499991.000000\n", - "25% 250725.500000 70162.000000 22865.250000 499998.000000\n", - "50% 495957.000000 188704.000000 37103.000000 500000.000000\n", - "75% 741076.500000 381478.750000 55836.000000 500001.000000\n", - "max 999860.000000 984005.000000 98912.000000 500007.000000" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "df.describe()" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
cmp_namecmp_bgtcmp_spentcmp_clickscmp_impruser
5047GRZ_20210217_20220406_35-45_B_GBP99986079154278932499999{\"username\": \"robertstephens\", \"name\": \"Jack J...
922AKX_20211111_20230908_40-50_M_GBP99985973968373078499996{\"username\": \"mark20\", \"name\": \"Ronald Rojas\",...
2113BYU_20220330_20220401_35-45_B_USD9996961179142961499998{\"username\": \"vkennedy\", \"name\": \"Rachel Lozan...
\n", - "
" - ], - "text/plain": [ - " cmp_name cmp_bgt cmp_spent cmp_clicks \\\n", - "5047 GRZ_20210217_20220406_35-45_B_GBP 999860 791542 78932 \n", - "922 AKX_20211111_20230908_40-50_M_GBP 999859 739683 73078 \n", - "2113 BYU_20220330_20220401_35-45_B_USD 999696 11791 42961 \n", - "\n", - " cmp_impr user \n", - "5047 499999 {\"username\": \"robertstephens\", \"name\": \"Jack J... \n", - "922 499996 {\"username\": \"mark20\", \"name\": \"Ronald Rojas\",... \n", - "2113 499998 {\"username\": \"vkennedy\", \"name\": \"Rachel Lozan... " - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# let's see which are the top 3 campaigns according\n", "# to budget (regardless of the currency)\n", @@ -384,87 +84,9 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
cmp_namecmp_bgtcmp_spentcmp_clickscmp_impruser
2876KTR_20210905_20230307_30-50_M_GBP108140749358499999{\"username\": \"kevin28\", \"name\": \"Karen Jackson...
1182AKX_20210527_20220128_35-45_B_EUR105439236790500000{\"username\": \"speters\", \"name\": \"Aaron Shelton...
1954BYU_20201018_20220904_25-40_F_GBP101722462568500002{\"username\": \"cdavis\", \"name\": \"Mrs. Kimberly ...
\n", - "
" - ], - "text/plain": [ - " cmp_name cmp_bgt cmp_spent cmp_clicks \\\n", - "2876 KTR_20210905_20230307_30-50_M_GBP 1081 407 49358 \n", - "1182 AKX_20210527_20220128_35-45_B_EUR 1054 392 36790 \n", - "1954 BYU_20201018_20220904_25-40_F_GBP 1017 224 62568 \n", - "\n", - " cmp_impr user \n", - "2876 499999 {\"username\": \"kevin28\", \"name\": \"Karen Jackson... \n", - "1182 500000 {\"username\": \"speters\", \"name\": \"Aaron Shelton... \n", - "1954 500002 {\"username\": \"cdavis\", \"name\": \"Mrs. Kimberly ... " - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# we can also use 'tail' to get the bottom 3 campaigns\n", "df.sort_values(by=['cmp_bgt'], ascending=False).tail(3)" @@ -479,82 +101,9 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
TypeStartEndTarget AgeTarget GenderCurrency
0GRZ2021-01-312021-04-1130-40FGBP
1BYU2021-01-092022-12-0430-35MGBP
2GRZ2021-11-242022-09-2120-35BEUR
\n", - "
" - ], - "text/plain": [ - " Type Start End Target Age Target Gender Currency\n", - "0 GRZ 2021-01-31 2021-04-11 30-40 F GBP\n", - "1 BYU 2021-01-09 2022-12-04 30-35 M GBP\n", - "2 GRZ 2021-11-24 2022-09-21 20-35 B EUR" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# first, let's explode cmp_name into its components\n", "# and get a separate DataFrame for those\n", @@ -578,7 +127,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -588,91 +137,9 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
cmp_nameTypeStartEndTarget AgeTarget GenderCurrency
0GRZ_20210131_20210411_30-40_F_GBPGRZ2021-01-312021-04-1130-40FGBP
1BYU_20210109_20221204_30-35_M_GBPBYU2021-01-092022-12-0430-35MGBP
2GRZ_20211124_20220921_20-35_B_EURGRZ2021-11-242022-09-2120-35BEUR
\n", - "
" - ], - "text/plain": [ - " cmp_name Type Start End Target Age \\\n", - "0 GRZ_20210131_20210411_30-40_F_GBP GRZ 2021-01-31 2021-04-11 30-40 \n", - "1 BYU_20210109_20221204_30-35_M_GBP BYU 2021-01-09 2022-12-04 30-35 \n", - "2 GRZ_20211124_20220921_20-35_B_EUR GRZ 2021-11-24 2022-09-21 20-35 \n", - "\n", - " Target Gender Currency \n", - "0 F GBP \n", - "1 M GBP \n", - "2 B EUR " - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# and take a peek: good! It seems to be ok.\n", "df[['cmp_name'] + campaign_cols].head(3)" @@ -680,7 +147,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -708,7 +175,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -718,83 +185,9 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
userUsernameEmailNameGenderAgeAddress
0{\"username\": \"susan42\", \"name\": \"Emily Smith\",...susan42vmckinney@leon.comEmily SmithF5366537 Riley Mission Apt. 337\\nNorth Jennifer, ...
1{\"username\": \"susan42\", \"name\": \"Emily Smith\",...susan42vmckinney@leon.comEmily SmithF5366537 Riley Mission Apt. 337\\nNorth Jennifer, ...
\n", - "
" - ], - "text/plain": [ - " user Username \\\n", - "0 {\"username\": \"susan42\", \"name\": \"Emily Smith\",... susan42 \n", - "1 {\"username\": \"susan42\", \"name\": \"Emily Smith\",... susan42 \n", - "\n", - " Email Name Gender Age \\\n", - "0 vmckinney@leon.com Emily Smith F 53 \n", - "1 vmckinney@leon.com Emily Smith F 53 \n", - "\n", - " Address \n", - "0 66537 Riley Mission Apt. 337\\nNorth Jennifer, ... \n", - "1 66537 Riley Mission Apt. 337\\nNorth Jennifer, ... " - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# and take a peek: good! Still in good shape.\n", "df[['user'] + user_cols].head(2)" @@ -802,7 +195,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -819,7 +212,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -838,82 +231,9 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
SpentClicksImpressionsCTRCPCCPI
017953525735000010.1051460.3414870.035906
1125884245754999990.0491505.1224420.251769
2480963396684999990.07933612.1247100.961928
\n", - "
" - ], - "text/plain": [ - " Spent Clicks Impressions CTR CPC CPI\n", - "0 17953 52573 500001 0.105146 0.341487 0.035906\n", - "1 125884 24575 499999 0.049150 5.122442 0.251769\n", - "2 480963 39668 499999 0.079336 12.124710 0.961928" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# let's take a peek\n", "df[['Spent', 'Clicks', 'Impressions',\n", @@ -922,19 +242,9 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CTR: 0.10514578970842059 0.10514578970842059\n", - "CPC: 0.3414870751146026 0.3414870751146026\n", - "CPI: 0.03590592818814362 0.03590592818814362\n" - ] - } - ], + "outputs": [], "source": [ "# let's take the values of the first row and verify\n", "clicks = df['Clicks'][0]\n", @@ -952,7 +262,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -969,74 +279,9 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
StartEndDurationDay of Week
02021-01-312021-04-1170Sunday
12021-01-092022-12-04694Saturday
22021-11-242022-09-21301Wednesday
\n", - "
" - ], - "text/plain": [ - " Start End Duration Day of Week\n", - "0 2021-01-31 2021-04-11 70 Sunday\n", - "1 2021-01-09 2022-12-04 694 Saturday\n", - "2 2021-11-24 2022-09-21 301 Wednesday" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# let's verify\n", "df[['Start', 'End', 'Duration', 'Day of Week']].head(3)" @@ -1044,7 +289,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1068,7 +313,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1078,7 +323,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1088,7 +333,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1112,7 +357,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1121,7 +366,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1134,189 +379,18 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
DurationBudgetClicksImpressionsSpentCTRCPCCPIAge
count5140.0000005140.0000005140.0000005140.0000005140.0000005140.0000005140.0000005140.0000005140.000000
mean365.923930496331.85505840414.236576499999.523346249542.7782100.0808299.8167490.49908655.503891
std213.233798289001.24189121704.1364802.010877219168.6364080.04340817.6498770.43833820.803059
min1.0000001017.000000355.000000499991.000000117.0000000.0007100.0035800.00023418.000000
25%180.000000250725.50000022865.250000499998.00000070162.0000000.0457301.7787240.14032538.000000
50%369.000000495957.00000037103.000000500000.000000188704.0000000.0742064.9775310.37740956.000000
75%553.000000741076.50000055836.000000500001.000000381478.7500000.11167311.6208500.76296273.000000
max730.000000999860.00000098912.000000500007.000000984005.0000000.197824517.2873241.96801490.000000
\n", - "
" - ], - "text/plain": [ - " Duration Budget Clicks Impressions Spent \\\n", - "count 5140.000000 5140.000000 5140.000000 5140.000000 5140.000000 \n", - "mean 365.923930 496331.855058 40414.236576 499999.523346 249542.778210 \n", - "std 213.233798 289001.241891 21704.136480 2.010877 219168.636408 \n", - "min 1.000000 1017.000000 355.000000 499991.000000 117.000000 \n", - "25% 180.000000 250725.500000 22865.250000 499998.000000 70162.000000 \n", - "50% 369.000000 495957.000000 37103.000000 500000.000000 188704.000000 \n", - "75% 553.000000 741076.500000 55836.000000 500001.000000 381478.750000 \n", - "max 730.000000 999860.000000 98912.000000 500007.000000 984005.000000 \n", - "\n", - " CTR CPC CPI Age \n", - "count 5140.000000 5140.000000 5140.000000 5140.000000 \n", - "mean 0.080829 9.816749 0.499086 55.503891 \n", - "std 0.043408 17.649877 0.438338 20.803059 \n", - "min 0.000710 0.003580 0.000234 18.000000 \n", - "25% 0.045730 1.778724 0.140325 38.000000 \n", - "50% 0.074206 4.977531 0.377409 56.000000 \n", - "75% 0.111673 11.620850 0.762962 73.000000 \n", - "max 0.197824 517.287324 1.968014 90.000000 " - ] - }, - "execution_count": 25, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "df.describe()" ] }, { "cell_type": "code", - "execution_count": 26, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAABCcAAAHCCAYAAADchKMyAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8rg+JYAAAACXBIWXMAAAxOAAAMTgF/d4wjAAB1/0lEQVR4nO3df3xU1Z3/8fdkMmMCIb8kAVFDgBqSiGQKNRIx0NpV2bhS2m5ZSBUqSmpBQaOtTXkIqKW2ttViqkhCpRUaWLTr1gqu39Vqg/HHonGiqJAtCKgYE5gkk4RMkknu9w+WqZEEE5I7dya8no8Hj0fm3jv38zmHyczJZ+49x2YYhiEAAAAAAACLRFidAAAAAAAAOLNRnAAAAAAAAJaiOAEAAAAAACxFcQIAAAAAAFiK4gQAAAAAALAUxQkAAAAAAGApihMAAAAAAMBSFCeAM8Rzzz0nl8slm82m1atXW50OAAAAAARQnAAsVltbK5fLpcTERNlsNrlcLrlcLo0fP15jx47V9ddfr5qamgHHueqqq+R2uwee8BdoaGjQ6tWrgxILAACEpsrKSn37298OjGsuvPBCfe1rX9PKlSsHZVwzEIxVgNBEcQKwWHJystxut2bPni1Jcrvdcrvd2r9/v/7jP/5D27ZtU35+vsVZ9l1DQ4PuvvtuPvABADhDVVVV6dJLL9VXv/pVvfXWW3K73Xr77bd1zTXX6N5779Xu3bstzY+xChCaKE4AIWzq1Kn6p3/6J7300ktqbm62Oh0AAIAvtGnTJg0bNky33HKLbDabJMlut6uwsFCTJ0+2ODsAoYriBBDi/H6/JMlms+m73/2uRo8eHfigl6Sf//znSklJkc1m04EDB7o99/e//70mTJigL33pS/ra176mXbt29Rjj9ddfV3Z2ts455xxlZ2froYce0le/+lXFxMTI5XLp8OHDkqTW1lbdcccdGjdunCZOnKjJkydr06ZNgfM89dRTysvLkyStXLkycClnQ0PDIPYIAAAIZR0dHWppadHRo0dP2vf888/rsssu06OPPqrMzEzZbDb9+te/Vn5+vlwul84++2zdeOONamlp6fa8d955R//8z/+scePGady4cbrmmmtUXV0d2L9y5Up96Utfks1m0xNPPKF58+YpMzNTF1xwgf70pz8FjmOsAoQwA0BIWLhwofH5X8nt27cbDofDuPnmmwPbVq1addJxGzduNCQZH3zwQWDbs88+a0gyHnvsMcMwDOPYsWPGvHnzDEnGqlWrAsd9+umnRmxsrPG9733P6OzsNAzDMH784x8bw4YNM2bOnNktTl5enjFhwgTj448/NgzDMF5++WXjrLPOMv7whz8Ejvnggw8MScbGjRtPtysAAEAYe+qppwxJxuTJk40//elPhs/n6/G4E2OGUaNGGW+++aZhGIZx8OBB47zzzjO++93vBo773//9XyM2NtZYsmSJ0dXVZXR1dRk333yzkZycbNTV1QWOe/HFFw1JxowZM4wjR44YhmEYRUVFxvDhww2Px3NSXMYqQGjhygkgxJyo4J9zzjn6xje+oRtvvFH3339/v89zzz33KD09Xddff70kKTo6WjfffPNJx/3mN79Rc3Oz1qxZo4iI428JK1eulN1u73bc888/rx07dujHP/6xxowZI0maPn265syZo1WrVvU7PwAAMDTNmTNHK1eu1Pvvv69vf/vbGjlypL71rW/pySefVHt7+0nHf+Mb39CUKVMkSSkpKVq2bJm2bNkSuDJi9erV8vv9uu+++2Sz2WSz2fTTn/5UjY2N+u1vf3vS+b75zW/q7LPPliR961vfUktLS69XjwIIHRQngBBzYkLMTz75RB988IH27t0rl8ulTz/9tM/n6Ozs1K5duwIf9CdMmjTppGNfffVVjR49OlBwkI4XMiZMmNDtuOeff17S8YLE58954MCBk24pAQAAZ667775bH330kR544AFdfPHFevrpp/Wd73xHU6dO1aFDh7ode+GFF3Z7PHXqVHV1den111+XdHwMMmnSJMXGxgaOiYuL03nnnaeXXnrppNhpaWmBnxMTEyWpX+MoANaItDoBAL0777zz9NBDD2nSpEm6//779etf/7pPzzty5Ij8fr/i4+O7bY+Lizvp2JqampOO6+nYI0eOSJLmzp3b7aqKY8eOadSoUTp69KhSU1P7lB8AABj6kpOTddttt+m2225TTU2NfvrTn+rhhx9WUVGR/vjHPwaO+2zRQZISEhIkKTDn1ZEjR9Ta2iqXy9XtuLa2tsDcXJ81bNiwwM8nrgrt7OwclDYBMA/FCSDEnbiC4b333pOkQGHAMIzAxJifnzRq5MiRcjgcqq+v77a9p8mezjnnHO3Zs+ek7Q0NDd2KFiNHjpQkbd++XSkpKafXGAAA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3jEMwzB+/etfB8YgeXl5xoQJE4yPP/7YMAzDePnll42zzjrL+MMf/hA4x9e//nVj8eLFRldXl2EYhvHMM890a0tfxiefH+cYhmH84Ac/MEaPHm3s3bs3kGtKSopx3XXX9dhPd911l2EYhtHU1GSkpaUZ8+fPDxzz97//3XA6ncbOnTsNwzCM9vZ2Y/78+SeN0wB0x5UTQAg6evSompubNWrUqEE97wsvvKC//OUv+slPfqLRo0dLkkaNGqXbb79dDoejx+fs2rVLP/jBD/TnP/9Z5557bmD7a6+9ptGjRys6OlqSNG7cOP3iF7/QeeedN6g5AwCA4GhubtYtt9wi6fj8FJdddpm2bNmixYsXS5JGjx6t3Nxcvfjiiz0+/8c//nFgXHD99dcrMzNTd99990nHff3rX9ekSZMkSd///vf14x//WM8//7x27NihH//4x4H5IKZPn645c+Zo1apVgee+9tprSk1NDVz5cPXVV+snP/lJt/39HZ9UV1fr0Ucf1c0336y0tDRJUmpqqm6//XZt2rRJlZWV3Y5vamoKXO0RExOjK664ottVEW+99Zba29s1fvx4SZLD4dCKFSt05ZVX9poDAG7rAM4o//3f/y1Juvjii7ttX7x4sX75y1+edHxlZaWuvPJKFRYWKiUlpdu+GTNm6L//+7919dVX689//rNaW1t1/fXX60tf+pJ5DQAAAKY5++yzFR8fH3icmJh40razzz5bNTU1PT7/wgsv7PZ46tSp+p//+R91dXV1256RkRH4efjw4Ro9erSef/55SccLEp81adIkHThwQAcOHJB0fPxx99136wc/+IFeeeUVdXV1ac2aNYHjT2d88te//lWGYZw0PsrOzpb0j/HTCSNHjlRiYmLgcWJioj799NPA44svvljR0dGaPn26HnjgAX344Ye68MILlZ+f32sOAChOACHp7LPP1vDhw7t90A2GI0eOSFK3D9RT+d73vqfx48dr5cqVam5u7rbvRz/6kf7whz+otrZWc+bM0ahRo3THHXeora1tUHMGAADBMWzYsG6PbTZbj9s6Ozt7fH5sbGy3xwkJCero6DhpPqqYmJiTnntijDJ37ly5XK7Av8cff1yjRo3S0aNHJUlPPvmkVqxYoR07dmj69OkaN25ct+VQT2d8ciJ2QkJCt+0nxksn9p/w+T6JiIjoVoAZO3asXn/9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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "df[['Budget', 'Spent', 'Clicks', 'Impressions']].hist(\n", " bins=16, figsize=(16, 6));" @@ -1324,20 +398,9 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAABCAAAAG1CAYAAAAof4UyAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8rg+JYAAAACXBIWXMAAAxOAAAMTgF/d4wjAABHV0lEQVR4nO3df3CU5b3//9dms2sCIb/qRm0/DUHb/ABKttBSI0QcpxUm1JTTVgZzFBEltVBBqcpgx+qpg/TY9niQViVJRZEGD/Ucz7GV6mirDUSZonGj2GIUNehXaSJ3djcJ2WQ3ub9/WLZEwhLc+85mk+djxhmy9533fd3XlXv32pf3D4dpmqYAAAAAAABslJLoBgAAAAAAgLGPAAIAAAAAANiOAAIAAAAAANiOAAIAAAAAANiOAAIAAAAAANiOAAIAAAAAANiOAAIAAAAAANiOAAIAAAAAANguNdENAGCPV199VXfffbdeffVVSVIwGFR+fr4qKir0L//yL9q4caP+9Kc/KTc3V5LU1dWlgwcP6vOf//yg1/7f//t/+s///E8tW7ZMb731lvr6+jR16lRJUk9PjyKRiC6++GLddddd8ng8idlZAAAwZpxqDlNUVKTKykr5fD699957Ou+885SRkaFQKCTTNFVRUaE77rhDWVlZp10XgM1MAGPOQw89ZGZlZZn/9V//Zfb395umaZoDAwPmY489ZmZkZJhZWVnmVVddZW7dujX6O88995wp6YTX5s2bF/153rx55uTJkwdt6+233zbPOecc88ILLzQHBgZs3CsAADDWDWcOc8zWrVtNSeZzzz0Xfe2vf/2reeaZZ5pf/epXo79/unUB2IdLMIAx5uWXX9a1116rX/ziF1q8eLFSUj4+zB0Oh77zne9o06ZNkqQvfelL+tznPhezVm5urmbPnh1znSlTpuiyyy5TQ0ODWltbrdkJAAAw7gx3DhNLSUmJVqxYoX379mnPnj2W1QVgDS7BAMaYDRs2KCMjQ0uXLh1y+eLFi/XCCy/ohz/84SlrzZgxQ3ffffcp14tEIpIkwzBUUFBwWu0FAACQhj+HOZX8/HxJ0uHDhy2tCyB+nAEBjCH9/f165plnNHPmTLlcriHXycjIUF1dnWXbbGpq0qOPPqozzzxTxcXFltUFAADjh5VzmDfffFOSVFRUlJC5EYCT4wwIYAw5cuSIurq6dNZZZ9m2jQ8++EBer1cDAwM6dOiQgsGgSktL9ctf/lITJkywbbsAAGDssmoO09DQoLq6OlVWVqq0tFRtbW22z40ADB8BBIDT8tnPflY+n0+SdPDgQX3rW9/SunXrNGfOnMQ2DAAAjEvXXntt9CkYOTk5uuWWW3TTTTclulkAhkAAAYwhn/nMZzRx4kT9/e9/H5HtnXfeebrxxht1xRVXqLS0VCUlJSOyXQAAMLbEM4epq6vTRRddZHldANbjHhDAGOJ0OjV//nw1NTUpHA4PuY5hGHrqqacUDAYt2eaVV14pj8eju+66y5J6AABg/LFrDpOIuRGAkyOAAMaY22+/XT09Pdq+ffuQyzds2KCVK1dadr8Gt9ut73//+3r00Ud5DCcAAPjU7JrDjPTcCMDJEUAAY8yMGTO0bds2/fCHP9Rjjz2mgYEBSVI4HNbmzZtVW1urBx98UKmp1l2Bdd1118npdOpnP/uZZTUBAMD4YtccJhFzIwBDc5imaSa6EQCs19zcrI0bN+q1116Ty+XSwMCAvF6v1q1bp2nTpg1ad9WqVXr66ad18OBBff7zn9esWbP0+OOPR5f7fD4tW7ZMb731lvr6+jR16lQtX75cq1evjq6zbNky7dixQyUlJfr5z3+ur3/96yO2rwAAYOwYzhymsrJSPp9P7733ns477zxlZGToL3/5i9xud1x1AdiLAAIAAAAAANgu7vOMfvWrX0UfySdJX/3qV1VdXS1J6u7uVm1trdLT09XR0aHKykpNnTpVkhSJRFRXVydJCgaDKi8vV1lZ2bC26fP55PV64206RgjjlVwYr+TCeCUXxmvk2TFPMU1T9fX1MgxD4XBYxcXFqqioGFZ7+BuIjf6Jjf6Jjf6Jjf6Jjf45OSv7xpILnWpra4d8fceOHSooKNCiRYtkGIbWr1+vzZs3y+12a9euXXI6nVqxYoVCoZDWrFmjkpISZWdnn3J7zc3N/HEkEcYruTBeyYXxSi6MV2JYPU/Zu3evWltbdeutt2pgYEA333yziouLde65556yLfwNxEb/xEb/xEb/xEb/xEb/nJyVfWPJTSjr6+u1bds2bdu2TYFAIPr67t27NXPmTElSbm6ucnJyov8XoqGhIbosLS1NhYWFamxstKI5AAAAUVbPU45flpKSotLSUjU0NIzgHgEAkJziDiC+8pWvqKKiQkuXLlVRUZF+8pOfqL+/X11dXerp6Rl0RkNWVpba2tokSe3t7Sdddirp6enxNhsjyOVyJboJOA2MV3JhvJILn18jz455yieXZWdnM4exCO9psdE/sdE/sdE/sdE/J2flZ1fcl2B87WtfG/Tv++67T62trcrLy4u3dJTP51Nzc7Okj3d+8eLFltWG/aqqqhLdBJwGxiu5MF7JZfHixdq5c6d6enokSaWlpZzuabORmKfEwhzm9PCeFhv9Exv9Exv9Exv9c3JWzl/iDiA++OADffazn/1nwdRU9fX1KSMjQ+np6fL7/crMzJQkBQKB6Ae+x+OR3++P/l4gEFBRUdGQ2/B6vSfs4OHDh8UDPJLDpEmT1NnZmehmYJgYr+TCeCUPh8Ohs88+my+gI8yOeconl/n9fnk8niG3zxzm9PCeFhv9Exv9Exv9Exv9MzSr5y9xBxCbN2/Wxo0bJUnvvvuuHA6HJk+eLEkqLy9XU1OT8vPzZRiGDMOIfggfWzZr1iyFQiG1tLRo+fLlw96uaZp8eNsgNdAhBQxLa4bPzJM5YZKlNWEvjq3kwngBJ2fHPKW8vFzPP/+8FixYoIGBATU3N2vVqlXDbhNzmNjom9jon9jon9jon9joH/s5zDh7+b777lM4HFZWVpYOHz6sRYsWqbi4WJLU1dWlmpoaTZw4UYZh6NJLL9X06dMlSeFwWLW1tXI4HAoGg5o7d67mzJkz7O1++OGH/IHYIPXQQfXeeaOlNSfcca96P1dgaU3YJzMzU8FgMNHNwDAxXsnD4XDonHPOSXQzxh075immaWr79u3q6OiIPoZz4cKFw24Tc5iT4z0tNvonNvonNvonNvpnaFbPX+IOIBKFD2972BFApP/kVxoI91laU1m5imTlWFsTknjzTTaMV/IggMAxzGFOjve02Oif2Oif2Oif2OifoVk9f4n7EgzgVMzuoHr/fb2lNc+47R6JAAIAAAAAkkbcj+EEAAAAAAA4FQIIAAAAAABgOwIIAAAAAABgO+4BAQAAMIJS339HZn9//IW4ITMAIMkQQAAAAIyg3n9fL7OnO+463JAZAJBsuAQDAAAAAADYjgACAAAAAADYjgACAAAAAADYjgACAAAAAADYjgACAAAAAADYjgACAAAAAADYjgACAAAAAADYjgACAAAAAADYjgACAAAAAADYjgACAAAAAADYjgACAAAAAADYjgACAAAAAADYjgACAAAAAADYjgACAAAAAADYLjXRDcCnlxrokAKGpTUdkYil9QAAAAAAkAggklvAUO+dN1paMu2WjZbWAwAAAABA4hIMAAAAAAAwAgggAAAAAACA7Sy7BOOJJ57Q9u3btXPnTklSd3e3amtrlZ6ero6ODlVWVmrq1KmSpEgkorq6OklSMBhUeXm5ysrKrGoKxgFHqkuphw5aXzgrV5GsHOvrAgASyqp5immaqq+vl2EYCofDKi4uVkVFRWJ2CgCAJGNJAHHo0CG9/vrrg17bsWOHCgoKtGjRIhmGofXr12vz5s1yu93atWuXnE6nVqxYoVAopDVr1qikpETZ2dlWNAfjQXeneu9eb3nZM267RyKAAIAxxcp5yt69e9Xa2qpbb71VAwMDuvnmm1VcXKxzzz03QXsHAEDyiPsSjEgkokcffVRVVVWDXt+9e7dmzpwpScrNzVVOTo58Pp8kqaGhIbosLS1NhYWFamxsjLcpAAAAg1g9Tzl+WUpKikpLS9XQ0DBCewMAQHKLO4D47W9/q4qKCqWnp0df6+rqUk9Pz6AzGrKystTW1iZJam9vP+kyAAAAq1g9T/nksuzsbOYwAAAMU1wBxBtvvKHe3l5Nnz7dqvYAAABYgnkKAACjS1z3gNi3b5+6u7tVU1OjUCgkSaqpqdGMGTOUnp4uv9+vzMxMSVIgEFBeXp4kyePxyO/3R+sEAgEVFRWddDs+n0/Nzc2SJJfLpaqqKk2aNCmepo8JvU6n9UUd1pe0pagt7ZScTqcm/ONvdrxyu93R4xajH+OVfOrr6xUOhyVJpaWl8nq9iW3QGGbHPOWTy/x+vzwez0nbMNQcxipj8TOL97TY6J/Y6J/Y6J/Y6J/YrJq/xBVAXHHFFdF/t7W1ac+ePaqurpYk7d+/X01NTcrPz5dhGDIMI9rI8vJyNTU1adasWQqFQmppadHy5ctPuh2v13vCDnZ2dso0zXian/RS+/utL2pLl9pQ1Kah7+/vVzAYtKd4ksjMzBz3fZBMGK/k4XA4lJGRYekXUMRmxzylvLxczz//vBYsWKCBgQE1Nzdr1apVJ23DUHMYq4zFzyze02Kjf2Kjf2Kjf2Kjf4Zm9fzFkqdgvP7663ruueckSb/+9a91ySWXaMmSJaqpqdGWLVtkGIZWr14tt9stSaqoqFBtba3uv/9+BYNBLV26VDk5PHkAAABYz8p5SllZmQ4ePKh7771X4XBYF198MU/AAABgmCwJIKZNm6Zp06bpBz/4waDX165dO+T6LpdLK1eutGLTgKUcqS6lHjpobdGsXEV4tCcAJIyV8xSHw6Err7zS8jYCADAeWBJAAGNGd6d6715vackzbrtHIoAAAAAAMM7F/RhOAAAAAACAUyGAAAAAAAAAtiOAAAAAAAAAtiOAAAAAAAAAtiOAAAAAAAAAtiOAAAAAAAAAtiOAAAAAAAAAtiOAAAAAAAAAtiOAAAAAAAAAtiOAAAAAAAAAtktNdAOAsc6R6lLqoYPWFs3KVSQrx9qaAAAAAGAjAgjAbt2d6r17vaUl0/7tl0oNGJbWJNQAAAAAYCcCCCAZ2RBqnHHbPRIBBAAAAACbcA8IAAAAAABgOwIIAAAAAABgOwIIAAAAAABgOwIIAAAAAABgOwIIAAAAAABgOwIIAAAAAABgOwIIAAAAAABgOwIIAAAAAABgOwIIAAAAAABgOwIIAAAAAABgu1QrimzdulU9PT2aOHGiWltbtWDBAs2ePVvd3d2qra1Venq6Ojo6VFlZqalTp0qSIpGI6urqJEnBYFDl5eUqKyuzojkAAABRVs9TTNNUfX29DMNQOBxWcXGxKioqErZ/AAAkC0sCiNTUVK1cuVKStH//fv3Hf/yHZs+erR07dqigoECLFi2SYRhav369Nm/eLLfbrV27dsnpdGrFihUKhUJas2aNSkpKlJ2dbUWTRpXUQIcUMCyv64hELK8JAMBYY/U8Ze/evWptbdWtt96qgYEB3XzzzSouLta5556b4D0FAGB0sySAuPLKK6P//uCDDzR58mRJ0u7du3XnnXdKknJzc5WTkyOfz6fZs2eroaFBl19+uSQpLS1NhYWFamxs1MKFC61o0ugSMNR7542Wl027ZaPlNQEAGGusnqc0NDRo5syZkqSUlBSVlpaqoaGBAAIAgFOwJICQpHfeeUf//d//rSNHjujmm29WV1eXenp6Bp3RkJWVpba2NklSe3v7SZcBAABYycp5yieXZWdn68CBAyOxGwAAJDXLbkI5ZcoU3XTTTbr88sv14x//WH19fVaVBgAAiAvzFAAAEi/uMyAGBgbU19entLQ0SdKMGTPU09Ojw4cPKz09XX6/X5mZmZKkQCCgvLw8SZLH45Hf74/WCQQCKioqGnIbPp9Pzc3NkiSXy6WqqipNmjQp3qaPmF6n057CjiSpaUdRW9ppU90kqel0OjUhM1Nutzt6zGL0Y7yST319vcLhsCSptLRUXq83sQ0a4+yYp3xymd/vl8fjGXL7Q81hrHLsfXss4T0tNvonNvonNvonNvonNqvmL3EHEB999JF27NihNWvWSJIMw1AoFJLH41F5ebmampqUn58vwzBkGEa0oceWzZo1S6FQSC0tLVq+fPmQ2/B6vSfsYGdnp0zTjLf5IyK1v9+ewnbsvi1dakNRu4Y+WfrUhpr9/f0KBoPKzMxUMBi0fgOwBeOVPBwOhzIyMiz9AopTs2OeUl5erueff14LFizQwMCAmpubtWrVqiG3P9QcxirH3rfHEt7TYqN/YqN/YqN/YqN/hmb1/CXuACIjI0MDAwO67777NHHiRL3//vv6/ve/L4/HoyVLlqimpkZbtmyRYRhavXq13G63JKmiokK1tbW6//77FQwGtXTpUuXk5MS9QwAAAMfYMU8pKyvTwYMHde+99yocDuviiy/mBpQAAAxD3AHEhAkTdOONQz/hISMjQ2vXrh1ymcvlij4SC8DYZctjaLNyFckisARwanbMUxwOx6AnawAAgOGx7CkYAJKbI9Wl1EMH1et0WnrZkCMSUWjjzZbVk6QzbrtHIoAAAAAAkgoBBICPdXeq9+71lpdNu2Wj5TUBAAAAJB/LHsMJAAAAAABwMgQQAAAAAADAdgQQAAAAAADAdgQQAAAAAADAdgQQAAAAAADAdgQQAAAAAADAdgQQAAAAAADAdgQQAAAAAADAdgQQAAAAAADAdgQQAAAAAADAdgQQAAAAAADAdgQQAAAAAADAdqmJbgAAnC5Hqkuphw5aWzQrV5GsHGtrAgAAAIgigACQfLo71Xv3ektLnnHbPRIBBAAAAGAbLsEAAAAAAAC2I4AAAAAAAAC2I4AAAAAAAAC2I4AAAAAAAAC2I4AAAAAAAAC2I4AAAAAAAAC2I4AAAAAAAAC2I4AAAAAAAAC2I4AAAAAAAAC2S423QGdnpx555BGlpaVJktrb23XVVVfp7LPPVnd3t2pra5Wenq6Ojg5VVlZq6tSpkqRIJKK6ujpJUjAYVHl5ucrKyuJtDgAAgCR75iimaaq+vl6GYSgcDqu4uFgVFRWJ2UEAAJJM3GdAHDlyRG63W8uXL9fy5cs1Y8YMPfDAA5KkHTt2qKCgQN/73vdUXV2tTZs2qa+vT5K0a9cuOZ1OXXfddVq9erUeeugh+f3+eJsDAAAgyZ45yt69e9Xa2qrrr79eN9xwg/74xz/q7bffTtQuAgCQVOIOIAoKCnTNNddEfz7rrLNkGIYkaffu3Zo5c6YkKTc3Vzk5OfL5fJKkhoaG6LK0tDQVFhaqsbEx3uYAwKfiSHUp9dBBa/8LdCR6t4BxzY45yvHLUlJSVFpaqoaGhpHaJQAAklrcl2BIksPhiP77pZde0vz589XV1aWenh5lZ2dHl2VlZamtrU3Sx6dBnmwZAIy47k713r3e0pJn3HaPlJVjaU0Ap8fqOconl2VnZ+vAgQO27gMAAGOFJQHEMU1NTerr61NFRYW6u7stq+vz+dTc3CxJcrlcqqqq0qRJkyyrb7dep9Oewo5TrzIqatpR1JZ22lR3PNe0q26S1HQ6nZqQmWl94eO43W5l2rwNWKu+vl7hcFiSVFpaKq/Xm9gGjRN2zVFOZag5jFVG4j1mpPGeFhv9Exv9Exv9Exv9E5tV8xfLAoimpibt27dPK1eulMPhUEZGhtLT0+X3+6MDGQgElJeXJ0nyeDyD7vkQCARUVFQ0ZG2v13vCDnZ2dso0Tauab6vU/n57Ctux+7Z0qQ1F7Rr6ZOnTZKlpV90kqdnf369gMGh94eNkZmbavg1Y49hno5VfQDE8Vs5RPrnM7/fL4/GcdNtDzWGsMhLvMSON97TY6J/Y6J/Y6J/Y6J+hWT1/seQxnC+++KKam5tVXV2tlJQUbd26VZJUXl6upqYmSZJhGDIMI/ohfPyyUCiklpYWzZkzx4rmxCU10GH5deCOSCTRuwUAwLhk9Rzl+GUDAwNqbm7WvHnzRnivAABITnGfAdHa2qpNmzZp0qRJeuGFFyRJR48e1dVXX60lS5aopqZGW7ZskWEYWr16tdxutySpoqJCtbW1uv/++xUMBrV06VLl5IyCa6UDhnrvvNHSkmm3bLS0HgAAODU75ihlZWU6ePCg7r33XoXDYV188cU699xzE7aPAAAkk7gDiMmTJ+vRRx8dcllGRobWrl075DKXy6WVK1fGu3kAAIAh2TFHcTgcuvLKKy1rIwAA44kll2AAAAAAAADEQgABAAAAAABsRwABAAAAAABsRwABAAAAAABsRwABAAAAAABsRwABAAAAAABsRwABAAAAAABsRwABAAAAAABsl5roBgDAWOVIdSn10EHrC2flKpKVY31dAAAAwEYEEABgl+5O9d693vKyZ9x2j0QAAQAAgCTDJRgAAAAAAMB2BBAAAAAAAMB2XIIBAEnm+HtL9DqdSu3vj78o95UAAACAzQggACDZ2HBvibR/+6VSA4alNQk1AAAAcDwCCACALaEGN8sEAADA8bgHBAAAAAAAsB0BBAAAAAAAsB0BBAAAAAAAsB0BBAAAAAAAsB0BBAAAAAAAsB0BBAAAAAAAsB0BBAAAAAAAsB0BBAAAAAAAsB0BBAAAAAAAsF1qvAUikYiefPJJPfbYY9qwYYPy8/MlSd3d3aqtrVV6ero6OjpUWVmpqVOnRn+nrq5OkhQMBlVeXq6ysrJ4mwIAADCI1fMU0zRVX18vwzAUDodVXFysioqKxOwcAABJJu4A4tlnn1VJSYl6e3sHvb5jxw4VFBRo0aJFMgxD69ev1+bNm+V2u7Vr1y45nU6tWLFCoVBIa9asUUlJibKzs+NtDgAAQJTV85S9e/eqtbVVt956qwYGBnTzzTeruLhY5557boL2EACA5BH3JRgLFixQYWHhCa/v3r1bM2fOlCTl5uYqJydHPp9PktTQ0BBdlpaWpsLCQjU2NsbbFAAAgEGsnqccvywlJUWlpaVqaGgYgT0BACD52XIPiK6uLvX09Aw6oyErK0ttbW2SpPb29pMuAwAAsFM885RPLsvOzmYOAwDAMHETSgAAAAAAYLu47wExlIyMDKWnp8vv9yszM1OSFAgElJeXJ0nyeDzy+/3R9QOBgIqKik5az+fzqbm5WZLkcrlUVVWlSZMm2dF09Tqd1hd1WF/Strq2tNWGouO9T5Olpl11x3NNu+raUNPpdGrCPz4D8E/19fUKh8OSpNLSUnm93sQ2aByKZ57yyWV+v18ej+ek2xpqDmOVsXiMud3u6JjgRPRPbPRPbPRPbPRPbFbNX2wJICSpvLxcTU1Nys/Pl2EYMgwj2shjy2bNmqVQKKSWlhYtX778pLW8Xu8JO9jZ2SnTNC1vd2p/v+U1ZX0z7atrS1ttKDre+zRZatpVdzzXtKuuDTX7+/sVDAatL5ykHA6HMjIyLP0Cik/v085TysvL9fzzz2vBggUaGBhQc3OzVq1addLtDDWHscpYPMYyMzPH3D5Zif6Jjf6Jjf6Jjf4ZmtXzl7gDiAMHDkRvzPT4449r9uzZKisr05IlS1RTU6MtW7bIMAytXr1abrdbklRRUaHa2lrdf//9CgaDWrp0qXJycuJtCgAAwCBWz1PKysp08OBB3XvvvQqHw7r44ot5AgYAAMMUdwBRXFys4uJiXXPNNYNez8jI0Nq1a4f8HZfLpZUrV8a7aQAAgJisnqc4HA5deeWVlrcTAIDxgJtQAgAAAAAA2xFAAAAAAAAA29l2E0oAwPjmSHUp9dBBa4tm5SqSxT2DAAAAkhEBBADAHt2d6r17vaUlz7jtHokAAgAAIClxCQYAAAAAALAdAQQAAAAAALAdAQQAAAAAALBdUt8DIjXQIQUMS2s6IhFL6wEAAAAAgCQPIBQw1HvnjZaWTLtlo6X1AAAAAAAAl2AAAAAAAIARQAABAAAAAABsl9yXYAAAxhVHqkuphw5aXzgrV5GsHOvrAgAAIIoAAgCQPLo71Xv3esvLnnHbPRIBBAAAgK24BAMAAAAAANiOAAIAAAAAANiOAAIAAAAAANiOAAIAAAAAANiOAAIAAAAAANiOAAIAAAAAANiOx3ACAAAkIUeqS6mHDsZfKCtXER5DCwAYAQQQAIBxz7IvcsfjSx3s1t2p3rvXx13mjNvukfhbBQCMAAIIAAAs+iJ3PL7UAQAADMY9IAAAAAAAgO0IIAAAAAAAgO0IIAAAAAAAgO0Seg+Ijz76SA8++KCys7NlGIaqqqqUn5+fyCYBAACcEnMYAABOX0LPgKitrdXcuXNVXV2tb3/729q8eXMimwMAADAszGEAADh9CTsDorOzUz6fTzfeeKMkqbCwUIZh6N1331VBQUGimgUAABDTWJvDWPoYWh4/CwCIIWEBRHt7u9xut9LS0qKvZWVlqa2tbVgf3g6HQw6nU470iZa2K1lq2lV3PNe0q+54rmlX3fFc066647mmXXUdTqccDsc/fz7u30hucc9h0idY0g6r/m4doaPq/c9/s6BFUtqtP5erK/Cpf7/vQ6dcA/1SZo4imdmWtGms4b0kNvonNvonNvrnRFb3icM0TdPSisP09ttv6/bbb9cjjzwSfW3t2rVasmSJZs+ePWhdn8+n5uZmSVJ6eroWL148om0FAMAqO3fuVE9PjySptLRUXq83sQ3CaWMOAwAYb6yavyTsDAiPx6O+vj6FQqHo/0EIBALyeDwnrOv1egft4M6dO/kATyL19fWqqqpKdDMwTIxXcmG8kgufX2MDcxj78J4WG/0TG/0TG/0TG/1zclZ+diXsJpSTJk2S1+tVU1OTJKmlpUU5OTmaMmXKKX/3WPKC5BAOhxPdBJwGxiu5MF7Jhc+vsYE5jH14T4uN/omN/omN/omN/jk5Kz+7EvoYzmuvvVZbt27V/v37deTIEV1//fWJbA4AAMCwMIcBAOD0JTSA8Hg8uuWWW07790pLS21oDezCeCUXxiu5MF7JhfEaO5jD2IP+iY3+iY3+iY3+iY3+OTkr+yZhN6EEAAAAAADjR8LuAQEAAAAAAMYPAggAAAAAAGA7AggAAAAAAGC7hN6E8pM++ugjPfjgg8rOzpZhGKqqqlJ+fv4J673wwgvas2ePMjMzJX18J+rU1I93Zf/+/fr973+vnJwc9fT0qLq6WhMmTBjR/RgvrBivNWvW6OjRo9F1r7jiCs2bN29kdmAcGe5Ytbe3a+vWrTIMQz/96U8HLePYGjlWjBfH1sgZzni99tpreuaZZ+TxeHTkyBGdeeaZqqqqUkrKx/8fgONr7BvucT2WRSIRPfnkk3rssce0YcOG6P53d3ertrZW6enp6ujoUGVlpaZOnRr9nbq6OklSMBhUeXm5ysrKErYPduns7NQjjzyitLQ0SR+/v1911VU6++yz6Z9/2Lp1q3p6ejRx4kS1trZqwYIFmj17Nv1znCeeeELbt2/Xzp07JXFsHfOrX/1KPp8v+vNXv/pVVVdXS6KPJKmvr087d+7UwMCAQqGQ2tvb9aMf/ci+vjFHkbvuustsbGw0TdM033jjDfOmm246YZ0jR46Y1dXVZk9Pj2maprllyxbzd7/7nWmaptnb22tee+215pEjR0zTNM3HH3/cfPDBB0eo9eNPvONlmqb5y1/+cmQaO84NZ6z6+/vNrVu3mr/73e/MdevWDVrGsTWy4h0v0+TYGknDGa+tW7eab775ZvTndevWmc8995xpmhxf48Vw/k7Guj/84Q/mG2+8YV522WVma2tr9PXa2lrz8ccfN03zn/OG3t5e0zRN8//+7//Mmpoa0zRNs6enx6yurjY7OjpGuum2e+edd8za2troz7t27TJvv/120zTpn2O2bdsW/fdrr71mXn311aZp0j/HtLa2mnfddZd52WWXRV+jbz4Wa05EH308Rzl48GD05wMHDpimaV/fjJpLMDo7O+Xz+TRz5kxJUmFhoQzD0LvvvjtovRdeeEGFhYXRhHjWrFn685//LEl65ZVX9JnPfEa5ubmSpJkzZ0aXwVpWjJckGYahbdu26aGHHtL//u//KhKJjNg+jBfDHauUlBQtW7ZMGRkZJ9Tg2Bo5VoyXxLE1UoY7XkuXLtUXvvCF6M95eXkyDEMSx9d4MNy/k7FuwYIFKiwsPOH13bt3R/smNzdXOTk50f9b2dDQEF2WlpamwsJCNTY2jlibR0pBQYGuueaa6M9nnXVW9D2C/vnYlVdeGf33Bx98oMmTJ0uif6SP/2/0o48+qqqqqkGv0zf/VF9fr23btmnbtm0KBALR18d7H/X19ampqUnvvPOO6uvrVVdXp6ysLEn29c2oCSDa29vldrujX1QlKSsrS21tbYPWa2trU3Z29pDrtLe3D1qWnZ2to0ePqqury9a2j0dWjJcklZWVqaqqSsuWLZNhGHrwwQdtb/t4M9yxOlUNjq2RYcV4SRxbI2W443XsUgtJCoVCevvtt3XhhRdGa3B8jW1WHddjUVdXl3p6eoY9txvL/eZwOKL/fumllzR//nz65xPeeecd/fznP9dzzz2n66+/nv75h9/+9reqqKhQenp69DX65p++8pWvqKKiQkuXLlVRUZF+8pOfqL+/nz7Sx9/VDh8+LIfDoaqqKs2bN0933HGHDMOwrW9GTQCB8enrX/969H4QF1100ZhLFYFE4dganUzTVF1dna6++mqdeeaZiW4OgFGoqalJfX19qqioSHRTRp0pU6bopptu0uWXX64f//jH6uvrS3STEu6NN95Qb2+vpk+fnuimjFpf+9rXol+Wv/a1r+mjjz5Sa2trYhs1SoRCIUnS+eefL0n64he/KJfLpQMHDti2zVETQHg8HvX19UU7QZICgYA8Hs+g9fLy8uT3+wetk5eXF61x/DK/368JEyac9BRlfHpWjNfRo0cHLUtNTVUkEtHAwICtbR9vhjtWp6rBsTUyrBgvjq2RczrjNTAwoLq6Op1//vmaNWvWoBocX2ObFcf1WJWRkaH09PRhz+3Ger81NTVp3759WrlypRwOB/3zD8dujnfMjBkz1NPTo8OHD4/7/tm3b5+6u7tVU1OjRx99VJJUU1Oj/fv3j/u+OeaDDz4Y9HNqaqr6+vo4vqTo5Z/Hn6mZmpoql8tlW9+MmgBi0qRJ8nq9ampqkiS1tLQoJydHU6ZM0WuvvaYPP/xQknTBBReopaUl+ib08ssvR09j/fKXv6wjR45Er5lramqKLoO1rBivd955R7///e+jNffv369p06YNOgAQv+GOVSwcWyPHivHi2Bo5wx2vSCSiBx54QOeff76+8pWvSPr4ju4Sx9d4EOvvBFJ5eXm0bwzDkGEY8nq9JywLhUJqaWnRnDlzEtVUW7344otqbm5WdXW1UlJSou8R9M/HT5HZsmVL9GfDMBQKheTxeMZ9/1xxxRVatWqVqqurtWTJEklSdXW1zj///HHfN8ds3rw5+u93331XDocjeg+R8d5Hubm5Ki4u1t/+9jdJH/dBMBhUYWGhbX3jME3TtH5XPp1jj5TLzs7WkSNHVFVVpcmTJ2vjxo2aNm2aKisrJUl79uxRY2Nj9LGOK1asiJ5q/Oqrr+rJJ59Ubm6ujh49qurqak2cODFh+zSWxTtex34/JydHTqdTfr9fy5YtiyZxsM5wx+p3v/udfD6fWltbVVZWpm9961vR08Q5tkZOvOPFsTWyhjNejzzyiJ566qlBj9b0er1atWqVJI6v8eBkfyfjyYEDB9TY2Kinn35ac+bM0ezZs1VWVqauri7V1NRo4sSJMgxDl156afR08nA4rNraWjkcDgWDQc2dO3fMfQGQpNbWVq1bt06TJk2Kvnb06FH95je/oX/0cV9s2bJFZ5xxhiZOnKj3339f8+bN09y5c+mff3j99df13HPPqaGhQfPnz9cll1yinJwc+kbSfffdp3A4rKysLB0+fFiLFi1ScXGxJPH3o48/n7Zv367c3Fy1t7frkksu0YwZM2zrm1EVQAAAAAAAgLGJ83EBAAAAAIDtCCAAAAAAAIDtCCAAAAAAAIDtCCAAAAAAAIDtCCAAAAAAAIDtCCAAAAAAAIDtCCAAAAAAAIDtCCAAAAAAAIDtCCAAAAAAAIDtCCAAAAAAAIDtCCAAAAAAAIDtCCAAAAAAAIDtCCAAAAAAAIDtCCAAAAAAAIDtCCAAAAAAAIDtCCCAceDVV1/VFVdcoRkzZmjGjBkqKCjQhRdeqJ/+9Kd64403JEmVlZXKz8+Xw+HQF77wBXm9XhUXF6uoqEg33nijAoGAJKmtrU1er1e5ublyOBzyer2qq6tL5O4BAAAASAIO0zTNRDcCgH0efvhhrVmzRjU1Nfrud7+rlJQUmaap//mf/9GyZcvkdDrl9/slSQ899JCuvvpqPffcc7roooskSX/729904YUXasqUKdq7d69SUj7OLZctW6aHH35YvIUAAAAAGA7OgADGsJdfflnXXnutfvGLX2jx4sXR8MDhcOg73/mONm3adMoaJSUlWrFihfbt26c9e/bY3WQAAAAAY1RqohsAwD4bNmxQRkaGli5dOuTyxYsX64UXXjhlnfz8fEnS4cOHLW0fAAAAgPGDMyCAMaq/v1/PPPOMZs6cKZfLNeQ6GRkZw7p/w5tvvilJKioqsrSNAAAAAMYPAghgjDpy5Ii6urp01llnxVWnoaFBdXV1qqysVGlpqUWtAwAAADDecAkGgBNce+21ysjIUCgUUk5Ojm655RbddNNNiW4WAAAAgCRGAAGMUZ/5zGc0ceJE/f3vfz/t362rq4s+BQMAAAAArMAlGMAY5XQ6NX/+fDU1NSkcDg+5jmEYeuqppxQMBke4dQAAAADGGwIIYAy7/fbb1dPTo+3btw+5fMOGDVq5cqUmTJgwwi0DAAAAMN5wCQYwhs2YMUPbtm3Tddddp0mTJunb3/62UlJSFA6H9cADD6i2tlZPPPGEUlN5KwAAAABgL4dpmmaiGwHAXs3Nzdq4caNee+01uVwuDQwMyOv1at26dZo2bZokqbKyUj6fT++9957OO+88ZWRk6C9/+YvcbvegWm1tbbrkkkt06NAhdXR0qLS0VD/4wQ907bXXJmLXAAAAACQJAggAAAAAAGC7YZ13HYlE9OSTT+qxxx7Thg0blJ+fr0gkoocfflj9/f1yu9368MMPddlll+kLX/hC9Hfq6uokScFgUOXl5SorK5Mkmaap+vp6GYahcDis4uJiVVRUDLvRPp9PXq/3NHcVicJ4JRfGK7kwXsmF8QIAAOPZsG5C+eyzz6qkpES9vb3R13p7e9XW1qbq6motW7ZM3/jGN3TPPfdEl+/atUtOp1PXXXedVq9erYceekh+v1+StHfvXrW2tur666/XDTfcoD/+8Y96++23h93o5ubmYa+LxGO8kgvjlVwYr+TCeAEAgPFsWAHEggULVFhYOOi1iRMnat26ddGfzzrrLPn9fg0MDEiSGhoaNHPmTElSWlqaCgsL1djYeMKylJQUlZaWqqGhIf69AQAAAAAAo1Jcj+FMSfnnr7/88sv6xje+EX2tvb1d2dnZ0eVZWVlqa2sbcll2dnZ02XCkp6fH02yMMJfLlegm4DQwXsmF8UoufH4BAIDxzJJn77399ts6cOCA1q5da0W5E/h8vuhpq+np6Vq8eLEt24E9qqqqEt0EnAbGK7kwXsll8eLF2rlzp3p6eiRJpaWl3BMCAACMG3EHEG+99ZZ+//vf64Ybbhj0uD6PxxO954MkBQIBFRUVDbnM7/fL4/GcdBter/eECdrhw4fFAzySw6RJk9TZ2ZnoZmCYGK/kwnglD4fDobPPPpsQHQAAjFtxBRB//etf9ac//UmrVq2Sy+XSE088oQsuuEBnnnmmysvL1dTUpFmzZikUCqmlpUXLly+XJJWXl+v555/XggULNDAwoObmZq1ateq0tm2aJgFEEmGskgvjlVwYLwAAACQDhzmMmeuBAwfU2Niop59+WnPmzNHs2bM1bdo0rVy5UmeccUb0vg+hUEi/+MUvlJeXp3A4rNraWjkcDgWDQc2dO1dz5syR9PFkefv27ero6Ig+hnPhwoWn1fAPP/yQSXeSyMzMVDAYTHQzMEyMV3JhvJKHw+HQOeeck+hmAAAAJMywAojRiAAiefAFKbkwXsmF8UoeBBAAAGC8i+spGAAAAAAAAMNBAAEAAAAAAGxHAAEAAAAAAGxHAAEAAAAAAGxHAAEAAAAAAGxHAAEAAAAAAGxHAAEAAAAAAGxHAAEAAAAAAGxHAAEAAAAAAGxHAAEAAAAAAGxHAAEAAAAAAGxHAAEAAAAAAGxHAAEAAAAAAGxHAAEAAAAAAGxHAAEAAAAAAGxHAAEAAAAAAGxHAAEAAAAAAGxHAAEAAAAAAGyXOpyVIpGInnzyST322GPasGGD8vPzJUnd3d2qra1Venq6Ojo6VFlZqalTp0Z/p66uTpIUDAZVXl6usrIySZJpmqqvr5dhGAqHwyouLlZFRYUd+wcAAAAAAEaBYQUQzz77rEpKStTb2zvo9R07dqigoECLFi2SYRhav369Nm/eLLfbrV27dsnpdGrFihUKhUJas2aNSkpKlJ2drb1796q1tVW33nqrBgYGdPPNN6u4uFjnnnuuLTsJAAAAAAASa1gBxIIFC4Z8fffu3brzzjslSbm5ucrJyZHP59Ps2bPV0NCgyy+/XJKUlpamwsJCNTY2auHChWpoaNDMmTMlSSkpKSotLVVDQ8NpBRCp778js79/2OsPS1auIlk51tYEAAAAAADDCyCG0tXVpZ6eHmVnZ0dfy8rKUltbmySpvb192Muys7N14MCB09p+77+vl9nT/WmbP6QzbrtHIoAAAAAAAMBy3IQSAAAAAADY7lOfAZGRkaH09HT5/X5lZmZKkgKBgPLy8iRJHo9Hfr8/un4gEFBRUdGQy/x+vzwez0m35fP51NzcLElyuVyqqqr6tM2Oyel0asI/9gXWcbvd0b8RjH6MV3JhvJJPfX29wuGwJKm0tFRerzexDQIAABghnzqAkKTy8nI1NTUpPz9fhmHIMIzoROrYslmzZikUCqmlpUXLly+PLnv++ee1YMECDQwMqLm5WatWrTrpdrxe74hM0Pr7+xUMBm3fzniTmZlJvyYRxiu5MF7Jw+FwKCMjw7YQHQAAYLRzmKZpnmqlAwcOqLGxUU8//bTmzJmj2bNnq6ysTF1dXaqpqdHEiRNlGIYuvfRSTZ8+XZIUDodVW1srh8OhYDCouXPnas6cOZI+fgzn9u3b1dHREX0M58KFC0+r4e9/d54t94CI5J9naU3wBSnZMF7JhfFKHg6HQ+ecc06imwEAAJAwwwogRiMCiOTBF6TkwnglF8YreRBAAACA8Y6bUAIAAAAAANsRQAAAAAAAANsRQAAAAAAAANsRQAAAAAAAANsRQAAAAAAAANsRQAAAAAAAANsRQAAAAAAAANsRQAAAAAAAANsRQAAAAAAAANsRQAAAAAAAANsRQAAAAAAAANsRQAAAAAAAANsRQAAAAAAAANsRQAAAAAAAANsRQAAAAAAAANsRQAAAAAAAANsRQAAAAAAAANsRQAAAAAAAANsRQAAAAAAAANulWlHklVde0R/+8Ad97nOf0+HDhzVv3jydf/756u7uVm1trdLT09XR0aHKykpNnTpVkhSJRFRXVydJCgaDKi8vV1lZmRXNAQAAAAAAo4wlAcR9992nNWvWaPr06Tp8+LDWrl2rmTNnaseOHSooKNCiRYtkGIbWr1+vzZs3y+12a9euXXI6nVqxYoVCoZDWrFmjkpISZWdnW9EkAAAAAAAwilhyCUZubq78fr8kye/3KyUlRQMDA9q9e7dmzpwZXScnJ0c+n0+S1NDQEF2WlpamwsJCNTY2WtEcAAAAAAAwylhyBsQNN9ygTZs26bXXXtNbb72ltWvXKhKJqKenZ9AZDVlZWWpra5Mktbe3n3QZAAAAAAAYW+IOIPr6+rRhwwatWrVKJSUl+uCDD7R582b98Ic/tKJ9AAAAAABgDIg7gDh06JACgYBKSkokSZ/97GfV29urgwcPKj09XX6/X5mZmZKkQCCgvLw8SZLH44letnFsWVFR0ZDb8Pl8am5uliS5XC5VVVXF2+whOZ1OTfhHW2Edt9sd/RvA6Md4JRfGK/nU19crHA5LkkpLS+X1ehPbIAAAgBESdwCRl5engYEBtbe3y+Px6OjRozpy5IjOPPNMlZeXq6mpSfn5+TIMQ4ZhRCdax5bNmjVLoVBILS0tWr58+ZDb8Hq9IzJB6+/vVzAYtH07401mZib9mkQYr+TCeCUPh8OhjIwM20J0AACA0c5hmqYZb5G9e/fqueee0znnnKMPP/xQM2bM0MKFC9XV1aWamhpNnDhRhmHo0ksv1fTp0yVJ4XBYtbW1cjgcCgaDmjt3rubMmTPsbb7/3Xkye7rjbfogZ9x2jyL551laE3xBSjaMV3JhvJKHw+HQOeeck+hmAAAAJIwlAUQiEEAkD74gJRfGK7kwXsmDAAIAAIx3ljyGEwAAAAAAIBYCCAAAAAAAYDsCCAAAAAAAYDsCCAAAAAAAYDsCCAAAAAAAYDsCCAAAAAAAYDsCCAAAAAAAYDsCCAAAAAAAYDsCCAAAAAAAYDsCCAAAAAAAYDsCCAAAAAAAYDsCCAAAAAAAYDsCCAAAAAAAYDsCCAAAAAAAYDsCCAAAAAAAYDsCCAAAAAAAYDsCCAAAAAAAYDsCCAAAAAAAYDsCCAAAAAAAYLtUK4r09fVp586dGhgYUCgUUnt7u370ox+pu7tbtbW1Sk9PV0dHhyorKzV16lRJUiQSUV1dnSQpGAyqvLxcZWVlVjQHAAAAAACMMpYEEPX19brwwgt17rnnSpLeeOMNSdKOHTtUUFCgRYsWyTAMrV+/Xps3b5bb7dauXbvkdDq1YsUKhUIhrVmzRiUlJcrOzraiSQAAAAAAYBSJO4Do6+tTU1OTPv/5z2vv3r06evSovvnNb0qSdu/erTvvvFOSlJubq5ycHPl8Ps2ePVsNDQ26/PLLJUlpaWkqLCxUY2OjFi5cGG+TPjVHqkuphw5aWzQrV5GsHGtrAgAAAACQZOIOINra2nT48GE5HA5VVVXpzTff1B133KG77rpLPT09g85oyMrKUltbmySpvb39pMsSprtTvXevt7TkGbfdIxFAAAAAAADGubhvQhkKhSRJ559/viTpi1/8olwulw4cOBBvaQAAAAAAMEbEfQZEbm6uJCkl5Z9ZRmpqqlwul9LT0+X3+5WZmSlJCgQCysvLkyR5PB75/f7o7wQCARUVFQ25DZ/Pp+bmZkmSy+VSVVVVvM0emsP6kk6nUxP+sf/jldvtjv4NYPRjvJIL45V86uvrFQ6HJUmlpaXyer2JbRAAAMAIsSSAKC4u1t/+9jd9+ctflmEYCgaDKiwsVHl5uZqampSfny/DMGQYRnSidWzZrFmzFAqF1NLSouXLlw+5Da/XOzITNNP6kv39/QoGg9YXTiKZmZnjvg+SCeOVXBiv5OFwOJSRkWFfiA4AADDKOUzTjPtrd3t7u7Zv367c3Fy1t7frkksu0YwZM9TV1aWamhpNnDhRhmHo0ksv1fTp0yVJ4XBYtbW1cjgcCgaDmjt3rubMmTPsbb7/3Xkye7rjbfogabdsVMiGe0BE8s+ztGay4QtScmG8kgvjlTwcDofOOeecRDcDAAAgYSx5DKfH49GNN954wusZGRlau3btkL/jcrm0cuVKKzY/qvFkDQAAAAAALAogEANP1gAAAAAAIP6nYAAAAAAAAJwKAQQAAAAAALAdAQQAAAAAALAdAQQAAAAAALAdAQQAAAAAALAdAQQAAAAAALAdAQQAAAAAALAdAQQAAAAAALAdAQQAAAAAALAdAQQAAAAAALAdAQQAAAAAALAdAQQAAAAAALAdAQQAAAAAALAdAQQAAAAAALAdAQQAAAAAALAdAQQAAAAAALAdAQQAAAAAALAdAQQAAAAAALBdqlWFnnjiCW3fvl07d+6UJHV3d6u2tlbp6enq6OhQZWWlpk6dKkmKRCKqq6uTJAWDQZWXl6usrMyqpgAAAAAAgFHGkgDi0KFDev311we9tmPHDhUUFGjRokUyDEPr16/X5s2b5Xa7tWvXLjmdTq1YsUKhUEhr1qxRSUmJsrOzrWgOPoXUQIcUMKwvnJUrZWZaXxcAAAAAkFTiDiAikYgeffRRVVVV6ZVXXom+vnv3bt15552SpNzcXOXk5Mjn82n27NlqaGjQ5ZdfLklKS0tTYWGhGhsbtXDhwnibMy44Ul1KPXTQ2pqRiEIbb7a0piSdcds90ucnW14XAAAAAJBc4g4gfvvb36qiokLp6enR17q6utTT0zPojIasrCy1tbVJktrb20+6DMPQ3aneu9dbWjLtlo2W1gMAAAAA4HhxBRBvvPGGent7NX36dFsDBJ/Pp+bmZkmSy+VSVVWVPRtyUNNqTqdTbrdbmVyGkTQYr+TCeCWf+vp6hcNhSVJpaam8Xm9iGwQAADBC4gog9u3bp+7ubtXU1CgUCkmSampqNGPGDKWnp8vv90cnxoFAQHl5eZIkj8cjv98frRMIBFRUVHTS7Xi93pGZoJnUtFp/f7/6+voUDAbt2QAsl5mZyXglEcYreTgcDmVkZNgXogMAAIxycQUQV1xxRfTfbW1t2rNnj6qrqyVJ+/fvV1NTk/Lz82UYhgzDiIYI5eXlampq0qxZsxQKhdTS0qLly5fH0xQAAAAAADCKpVhR5PXXX48+fvPXv/613nvvPS1ZskRvv/22tmzZoi1btmj16tVyu92SpIqKCoXDYd1///3atGmTli5dqpycHCuaAgAAAAAARiFLHsM5bdo0TZs2TT/4wQ8Gvb527doh13e5XFq5cqUVmwYAAAAAAEnAkjMgAAAAAAAAYiGAAAAAAAAAtiOAAAAAAAAAtiOAAAAAAAAAtiOAAAAAAAAAtrPkKRjAyThSXer926tK7e+3rmhWriJZPLYVAAAAAJIJAQTs1d2po3evt7TkGbfdIxFAAAAAAEBS4RIMAAAAAABgOwIIAAAAAABgOwIIAAAAAABgOwIIAAAAAABgOwIIAAAAAABgOwIIAAAAAABgOwIIAAAAAABgu9RENwA4XY5Ul1IPHbS2aFauIlk51tYEAAAAAEQRQCD5dHeq9+71lpZM+7dfKjVgWFqTUAMAAAAA/okAApBsCTXOuO0eiQACAAAAACRxDwgAAAAAADACCCAAAAAAAIDt4r4Eo7OzU4888ojS0tIkSe3t7brqqqt09tlnq7u7W7W1tUpPT1dHR4cqKys1depUSVIkElFdXZ0kKRgMqry8XGVlZfE2BwAAAAAAjEJxnwFx5MgRud1uLV++XMuXL9eMGTP0wAMPSJJ27NihgoICfe9731N1dbU2bdqkvr4+SdKuXbvkdDp13XXXafXq1XrooYfk9/vjbQ4AAAAAABiF4j4DoqCgQNdcc03057POOkuG8fHTBHbv3q0777xTkpSbm6ucnBz5fD7Nnj1bDQ0NuvzyyyVJaWlpKiwsVGNjoxYuXBhvk4BRgceFAgAAAMA/WfIUDIfDEf33Sy+9pPnz56urq0s9PT3Kzs6OLsvKylJbW5ukjy/VONkyYEzgyRoAAAAAEGXpYzibmprU19eniooKdXd3W1bX5/OpublZkuRyuVRVVWVZ7UEcp16FmqOg7jiu6XQ6NSEz0/rCx3G73cq0eRuwDuOVfOrr6xUOhyVJpaWl8nq9iW0QAADACLEsgGhqatK+ffu0cuVKORwOZWRkKD09XX6/Pzo5DgQCysvLkyR5PJ5B93wIBAIqKioasrbX6x2ZCZpJzaSoO45rDjhSdHT/K9YXPu7SjszMTAWDQeu3AVswXsnj2GejbSE6AADAKGdJAPHiiy/qwIEDqq6ulsPh0NatW3X11VervLxcTU1Nys/Pl2EYMgwjGiQcWzZr1iyFQiG1tLRo+fLlVjQHGLtsuKxD4tIOAAAAAPaLO4BobW3Vpk2bNGnSJL3wwguSpKNHj+rqq6/WkiVLVFNToy1btsgwDK1evVput1uSVFFRodraWt1///0KBoNaunSpcnL4AgQAAAAAwFgUdwAxefJkPfroo0Muy8jI0Nq1a4dc5nK5tHLlyng3D8ACxz+xo9fpVGp/f/xFeWIHAAAAgONYehNKAEmKJ3YAAAAAsFlKohsAAAAAAADGPgIIAAAAAABgOwIIAAAAAABgO+4BAcAWx9/Y0jLc2BIAAABIWgQQAOzBjS0BAAAAHIcAAkDS4KwKAAAAIHkRQABIHpxVAQAAACQtAggA4xpnVQAAAAAjgwACwPjGWRUAAADAiOAxnAAAAAAAwHYEEAAAAAAAwHZcggEAFrPlvhIS95YAAABAUiOAAACr2XBfCUlK+7dfKjVgDHqt1+lUan//py9KqAEAAIARQgABAMmCG2YCAAAgiXEPCAAAAAAAYDvOgACAccyW+1VwWQcAAACGQAABAOMZl3UAAABghHAJBgAAAAAAsF1Cz4D46KOP9OCDDyo7O1uGYaiqqkr5+fmJbBIAIE5c1gEAAIChJDSAqK2t1bx583TBBReopaVFmzdv1s9+9rNENgkAEC8bLusY6hGkliDYAAAAGDEJCyA6Ozvl8/l04403SpIKCwtlGIbeffddFRQUJKpZAIDRyIZQQ+J+FQAAACMpYQFEe3u73G630tLSoq9lZWWpra1tWAGEI32C5W1yOJ1ypE+k5iivS03GaTzWtKvueK4pSSnuNLn+v3etLZqZo0hm9gkvOxwOa7cDAACQZBymaZqJ2PDbb7+t22+/XY888kj0tbVr12rJkiWaPXv2oHV9Pp+am5slSenp6Vq8ePGIthUAAKvs3LlTPT09kqTS0lJ5vd7ENggAAGCEJOwMCI/Ho76+PoVCoehZEIFAQB6P54R1vV7voAnazp07CSGSSH19vaqqqhLdDAwT45VcGK/kwucXAAAYzxL2GM5JkybJ6/WqqalJktTS0qKcnBxNmTLllL977P8cITmEw+FENwGngfFKLoxXcuHzCwAAjGcJfQrGtddeq61bt2r//v06cuSIrr/++kQ2BwAAAAAA2CShAYTH49Ett9xy2r9XWlpqQ2tgF8YruTBeyYXxSi6MFwAAGM8SdhNKAAAAAAAwfiTsHhAAAAAAAGD8IIAAAAAAAAC2I4AAAAAAAAC2S+hNKGP56KOP9OCDDyo7O1uGYaiqqkr5+fknrPfCCy9oz549yszMlPTxkzVSU0ftbo1Zwx2vf/3Xf9WECROiP69Zs0bTp08fyaZCUiQS0ZNPPqnHHntMGzZsGHKsJI6v0WC4Y8WxNTp0dnbqkUceUVpamiSpvb1dV111lc4+++wT1uX4AgAA482onenU1tZq3rx5uuCCC9TS0qLNmzfrZz/72aB1DMPQww8/rE2bNiktLU01NTV66qmn9M1vfjNBrR6/hjNeknTBBRdo1apVCWghjvfss8+qpKREvb29J12H42t0GM5YSRxbo8WRI0fkdru1fPlySdIf/vAHPfDAA7rjjjsGrcfxBQAAxqNReQlGZ2enfD6fZs6cKUkqLCyUYRh69913B633wgsvqLCwMPp/mmbNmqU///nPI93ccW+44yVJ77//vrZt26Zf//rXeuaZZ8RDWBJjwYIFKiwsjLkOx9foMJyxkji2RouCggJdc8010Z/POussGYZxwnocXwAAYDwalWdAtLe3y+12RydmkpSVlaW2tjYVFBREX2tra1N2dvYJ62BkDXe8JOmiiy7S/PnzNTAwoJ///Ofq7u7WokWLRrbBGBaOr+TCsTV6OByO6L9feuklzZ8//4R1OL4AAMB4NCrPgMDYdWwinpKSonnz5mnPnj0JbhEwNnBsjT5NTU3q6+tTRUVFopsCAAAwKozKAMLj8aivr0+hUCj6WiAQkMfjGbReXl6e/H7/oHXy8vJGqpn4h+GOVyAQ0NGjR6M/p6amqq+vb8TaidPD8ZU8OLZGn6amJu3bt08rV64cdEbEMRxfAABgPBqVAcSkSZPk9XrV1NQkSWppaVFOTo6mTJmi1157TR9++KEkRW94eOyL78svv6wLL7wwYe0er4Y7Xq+88op2794d/b39+/frS1/6UkLajKFxfCUPjq3R68UXX1Rzc7Oqq6uVkpKirVu3SuL4AgAAcJij9E5l7e3t2rp1q7Kzs3XkyBFVVVVp8uTJ2rhxo6ZNm6bKykpJ0p49e9TY2Bh9jNmKFSt4jFkCDGe83n33Xe3YsUNnnXWWIpGIwuGwrr766kGPDsTIOHDggBobG/X0009rzpw5mj17tsrKyji+RqHhjBXH1ujR2tqqdevWadKkSdHXjh49qt/85jccXwAAYNwbtQEEAAAAAAAYO0blJRgAAAAAAGBsIYAAAAAAAAC2I4AAAAAAAAC2I4AAAAAAAAC2I4AAAAAAAAC2I4AAAAAAAAC2I4AAAAAAAAC2I4AAAAAAAAC2I4AAAAAAAAC2+/8Bg09o2G3dnNEAAAAASUVORK5CYII=\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "df[['CTR', 'CPC', 'CPI']].hist(\n", " bins=20, figsize=(16, 6));" @@ -1345,20 +408,9 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAABCcAAAHCCAYAAADchKMyAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8rg+JYAAAACXBIWXMAAAxOAAAMTgF/d4wjAABq+UlEQVR4nO3de3xU9Z3/8feQCwnmDrmAGoJoSCKSKVQEMdDqqmxckVZlIVUQkWhRQaNbZfkVvBRt3d4oVSChUoUNFLFUK3HdxdqCoBaNExUFqnLRQi4ySSb3ZJLz+4MyNQYwMDPnzExez8eDxyNzzpnz+X4/TGY+88m52AzDMAQAAAAAAGCRflYPAAAAAAAA9G00JwAAAAAAgKVoTgAAAAAAAEvRnAAAAAAAAJaiOQEAAAAAACxFcwIAAAAAAFiK5gQAAAAAALAUzQmgj3jllVdkt9tls9n00EMPWT0cAAAAAPCgOQFYrLq6Wna7XUlJSbLZbLLb7bLb7TrvvPM0dOhQzZ49W5WVlV7Hufrqq+VwOLwf8Neoq6vTQw89ZEosAAAQmMrLy3X99dd76poLL7xQ3/72t7V48WKf1DXeoFYBAhPNCcBiKSkpcjgcmjJliiTJ4XDI4XDo008/1e9//3tt3LhRBQUFFo+y9+rq6vTwww/zgQ8AQB9VUVGhSy+9VN/61rf07rvvyuFw6L333tO1116rRx99VB988IGl46NWAQITzQkggI0ZM0b/8i//oj//+c9qbGy0ejgAAABfa+3atRowYIDuvvtu2Ww2SVJYWJiKioo0atQoi0cHIFDRnAACnNvtliTZbDZ973vfU1pamueDXpJ+/OMfKz09XTabTQcOHOj23N/+9rcaPny4zj//fH3729/Wrl27Thjjrbfe0tixYzV48GCNHTtWv/rVr/Stb31LMTExstvtOnz4sCSppaVF999/v4YNG6YRI0Zo1KhRWrt2rWc/mzdvVn5+viRp8eLFnkM56+rqfJgRAAAQyDo6OtTU1KSjR4/2WLd161ZddtllWrlypXJycmSz2fSzn/1MBQUFstvtGjhwoG677TY1NTV1e97777+vf/3Xf9WwYcM0bNgwXXvttdq3b59n/eLFi3X++efLZrPpueee0/Tp05WTk6MLLrhAzz//vGc7ahUggBkAAsKsWbOMr/5KbtmyxYiIiDDuuusuz7IlS5b02G7NmjWGJGP//v2eZS+//LIhyXj66acNwzCM5uZmY/r06YYkY8mSJZ7tqqqqjLi4OOOWW24xOjs7DcMwjAcffNAYMGCAMWnSpG5x8vPzjeHDhxt///vfDcMwjNdff93o37+/8cwzz3i22b9/vyHJWLNmzZmmAgAABLHNmzcbkoxRo0YZzz//vNHa2nrC7Y7XDKmpqcY777xjGIZhHDx40DjnnHOM733ve57t/va3vxlxcXHGvHnzjK6uLqOrq8u46667jJSUFKOmpsaz3WuvvWZIMiZOnGh88cUXhmEYxsKFC42zzjrLcDqdPeJSqwCBhSMngABzvIM/ePBgXXfddbrtttv0xBNPnPZ+HnnkEWVlZWn27NmSpOjoaN111109tvvlL3+pxsZGLV26VP36HXtLWLx4scLCwrptt3XrVpWVlenBBx/UkCFDJEkTJkzQ1KlTtWTJktMeHwAACE1Tp07V4sWL9dFHH+n666/XoEGD9N3vflebNm1Se3t7j+2vu+46jR49WpKUnp6u+fPna/369Z4jIx566CG53W49/vjjstlsstls+tGPfqT6+nr9+te/7rG/73znOxo4cKAk6bvf/a6amppOevQogMBBcwIIMMcviHnkyBHt379fe/fuld1uV1VVVa/30dnZqV27dnk+6I8bOXJkj23feOMNpaWleRoO0rFGxvDhw7ttt3XrVknHGhJf3eeBAwd6nFICAAD6rocffliff/65fv7zn+viiy/Wiy++qBtvvFFjxozRoUOHum174YUXdns8ZswYdXV16a233pJ0rAYZOXKk4uLiPNvEx8frnHPO0Z///OcesTMzMz0/JyUlSdJp1VEArBFu9QAAnNw555yjX/3qVxo5cqSeeOIJ/exnP+vV87744gu53W4lJCR0Wx4fH99j28rKyh7bnWjbL774QpI0bdq0bkdVNDc3KzU1VUePHlVGRkavxgcAAEJfSkqK7r33Xt17772qrKzUj370Iz355JNauHCh/vu//9uz3ZebDpKUmJgoSZ5rXn3xxRdqaWmR3W7vtl1bW5vn2lxfNmDAAM/Px48K7ezs9MmcAPgPzQkgwB0/guHDDz+UJE9jwDAMz4Uxv3rRqEGDBikiIkK1tbXdlp/oYk+DBw/Wnj17eiyvq6vr1rQYNGiQJGnLli1KT08/s8kAAICQ9/bbb2vAgAHKycnxLEtLS9Ovf/1r/d///Z/efffdbtu7XK5uj51OpyR5juocNGiQcnJy9Kc//cnPIwdgJU7rAALcZ599JklKTU2VdOyvEJK6NR727t3b7TlhYWG6+OKLVV5e3m357t27e+x//Pjxqqqq8vx1Qjp2V45PP/2023ZXXnmlpGP3Lv+yv//97/r3f/93zzmkERERko41TyTpnXfe6XY1bQAAENpeeuklPfvssydcZ7PZPH/wOO6r9ck777yjfv36aezYsZKO1SAffvihOjo6um23du1aLV++/LTHR60CBCaaE0AAa2xs1H/+538qPDxchYWFkqSJEyfKZrNp8+bNkqRPP/30hH9JWLJkifbu3as1a9ZIOtZw+MlPftJju3vvvVexsbFatGiRurq6JEk/+tGPFBkZ2W27K664Qtdee60WL16syspKSceO2LjnnnuUmprq2T41NVXR0dH6/PPPJUkLFizQm2++6Yt0AACAILFixQr95S9/8Tx2u9366U9/qr179+rOO+/stu3WrVs9R1McOnRIv/71rzVjxgyNGDFC0rELYra1tWnJkiWehsIHH3yghQsX6uKLLz7tsVGrAIHJZhz/DQdgierqal111VU6dOiQamtrlZubK+nYh3hDQ4Muuugi/eAHP9DEiRM9z/nVr36ln/3sZ0pISNAll1wiu92uO++8U9nZ2SoqKtJtt90mSXr22Wf18MMPSzr2Qfzoo4/qX/7lX5SamqrRo0errKxMkrRr1y7dddddOnTokDIyMnTHHXdozZo1stlseu211zxxjxcGv/vd7xQTE6Pw8HB95zvf0aJFi7pdh2LVqlV67LHHFBsbqwsuuEDr169XVFSU33MJAACst3fvXj377LN69dVX1dLSoq6uLtXX12v48OFasGCBpk6dKkk6cOCAhg0bpuXLl+vdd99VeXm5Dh06pKlTp+pXv/qVzjrrLM8+P/zwQz3wwANyOBxKSUlRbGysfvjDH+qKK66QJP3iF7/Qk08+qU8++UTDhw/XokWLdO6552r+/Pn66KOPdO6556qgoEA//vGPJVGrAIGI5gSAExo1apSys7P1u9/9zuqhAACAEHS8ObFmzRrdcsstVg8HgMV6dVqH2+3WCy+8oJtvvtlz6x+3263f/OY3Ki4u1m9/+1s9/vjj+vjjj7s9Z+XKlVq5cqWeeOIJvfHGG6c1MIfDcVrb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ralTpyo1NVX333+/2trafDpmAABgjgEDBnR7bLPZTriss7PzhM+Pi4vr9jgxMVEdHR09rkcVExPT47nHa5Rp06bJbrd7/j377LNKTU3V0aNHJUmbNm3SokWLVFZWpgkTJmjYsGHdbod6JvXJ8diJiYndlh+vl46vP+6rOenXr1+3BszQoUP11ltvafz48Vq0aJHS09N1xRVX6MMPPzzpGADQnAACUlhYmK6++mqVl5ero6PjhNs4nU79z//8j1wuV6/3O2jQIElSbW1tr7Z/7rnntG7dOlVWVmrhwoU91s+cOVO7du3S7t27NW3aNP3sZz/To48+2uvxAACA0PHVmsTpdCoiIkLJyclf+9zjNcqWLVvkcDg8//bt26fKykqNGTNG0rHGwOLFi3XgwAG9+uqrGjp0qObOnes58kI6/frkeGyn09lj/F9efzouuugilZaWqrKyUk8++aQcDocmT57c4ygSAP9EcwIIUEuWLFFLS4vWrVt3wvVLly7VvHnzenTvT+XKK6+UJL399tvdlj/77LO67777emw/YsQIZWdn64c//KGefPJJ7dixw7Nu4cKFnkMsc3JytHr1al100UWnvOsHAAAIXbt37+72+J133tHYsWPVr9/Xf+U4XqNUVFR0W3787hzt7e2SpBkzZkg6dgTH5Zdf7rlt+vH640zqkyuuuEI2m027du3qtvz44+Nj661XX33VczRHfHy85s2bp0WLFumzzz5TXV3dae0L6EtoTgABatSoUZ6mwaZNmzyd9o6ODi1fvlwlJSV6+umnFR4e3ut9XnHFFbr22mv12GOPqbq6WpL0+eef6+GHHz7lRZoeeOAB5ebmas6cOWptbZUkvfHGG1q2bJlnXAcPHtTnn3+uyy+//EynDAAAgtivf/1rtbS0SJLWrFmjjz76qNvFLE/leI2yePFizzUtmpqadM899yg1NVWRkZGSpA0bNuj3v/+953mvv/66wsLCNHHiRElnVp9ccMEFuuOOO/Tkk09q3759kqRDhw7p5z//uW6++WaNHj36tPLw2Wef6Sc/+Ynn9Fy326233npLubm5vT61FuiTrL5dCIBTczgcxr//+78bOTk5Rm5urnHRRRcZN998s/HBBx8YhmEYu3btMnJzc42IiAgjMTHRGDNmjGEYhjF27FgjMTHRiIiIMHJzc413333XMAzDaG1tNR54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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "# let's see the campaigns whose spent is > than 75% of the budget\n", "selector = (df.Spent > df.Budget * .75)\n", @@ -1368,22 +420,11 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": null, "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "image/png": "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REREpLVE1UqMFStWkJeXR11dXYuO37x5M2vXrmXGjBnccsst9OzZk5dffrmNoxQRERERERGRthBVKzEmTpzY7Pnbtm1j5cqVpKenc/DgQa6++mq6detGWloagUAAn89HUlISVVVVpzdgEREREREREWk1UVXEaE5NTQ1PPvkkjz76KImJiWzZsoWnnnqKf//3f6dPnz5cffXVzJw5k6ysLGpqarjnnnsiHbKIiIiIiIiI/ABRX8T44osvqKurY968eUCob4ZlWViWRXFxMcuXL+ehhx4iPj6eV199lRUrVjB16tQIRy0iIiIiIiIiJyvqixgATqeT6dOnh0/7fD4Mw2DDhg3k5eURHx8PQGFhITNnzlQRQ0RERERERCQKtaixZzAY5M033+SGG27g66+/bvYY0zR54YUXeOqpp3j++eeZPXs2fr8/fPmMGTP4+c9/Hv5v5cqVJxVocXFxs+f369cPt9vNgQMHAKiqquLRRx8FIDc3l/3794eP3b9/vyaTSKs5UU6KRIpyUuxIeSl2o5wUu1FOih3ZOS9btBKjJVNBVqxYwd69e/ntb38LwPz583njjTeYNm0aECo23HXXXT840M2bN5OYmMiaNWsAeP311xk+fDgjR47kn/7pn3j++efJycnB7XZz6623AnDppZdSWlrK//zP/5CSkkJJSckpxSByrM2bN1NQUBDpMETClJNiR8pLsRvlpNiNclLsyM552aIixommghxr37595OTkhE93796dxYsXh4sYlZWVzJs3D9M0SUtLY9KkScTGntxulv79+9O/f39+9rOfNTo/Pz+f/Pz8JsfHxsZy2223ndRtiIiIiIiIiIg9tWg7SUvk5eXx+eefEwwGAdi+fTsVFRXhy0eOHElRURE333wzlZWVzJkz56SuPykpqbVCFWkVcXFxkQ5BpBHlpNiR8lLsRjkpdqOcFDuy8+dvw7Isq6UHT5s2jVmzZtGzZ89mL3/vvff48ssvSU1NJTk5mbfffptnn322yXG7du3igQceYO7cuSe8reLiYjZv3gyEHsCjKzpEREREREREpG0tXLgQr9cLwODBg22zvaRVp5OMGzeOcePGAbB27Vq6d+8OgMfjwe/3k5aWFrrR2FiCwSCmaeJwNL8YpKCgoMmDdODAAU6i5iLSplJSUqipqYl0GCJhykmxI+Wl2EW9abHrsA8jLpEe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nY8//hiAJUuW8Omnn4aPCwQCbN++ncLCwka/n5GRwQ033MDjjz/OU089RX19PZdddllr3QcRERERERER6QAMy7KsSAfREsXFxbZawiKinBS7UU6KHSkvxW6Uk2I3ykmxIzvnZdQUMURERERERESkY2v1xp4iIiIiIiIiIm1BRQwRERERERERiQoqYoiIiIiIiIhIVFARQ0RERERERESigooYIiIiIiIiIhIVVMQQERERERERkagQG+kATkYwGGTp0qUsWrSIBx98kJ49e37n8QsXLmTZsmU4HKFajWma5ObmMnPmzNMRroiIiIiIiIi0oqgqYqxYsYK8vDzq6upadHxCQgKPPPIImZmZ4d83TbMtQxQRERERERGRNhJVRYyJEyc2e/62bdtYuXIl6enpHDx4kKuvvppu3boxZcqURsd9+OGH3HfffacjVBERERERERFpZVHfE6OmpoYnn3ySn/3sZxQVFTFu3DieeuqpJsd99tln9OnTh8TExAhEKSIiIiIiIiKnKqpWYjTniy++oK6ujnnz5gGhvhmWZWFZFoZhhI9btmwZ11xzTaTCFBEREREREZFTFPVFDACn08n06dPDp30+X6MCRkVFBX6/n5ycnEiEJyIiIiIiIiKtoMVFjOeeew6v14vT6WTv3r1MnDiR4cOHNzmuvLyc5557jsrKSv70pz81umzbtm0sWbKE9PR0vF4v06dPJzk5+ZTuQL9+/XC73Rw4cICuXbtSVVXFk08+yW9+85vwMcuXL+eSSy45pdsRERERERERkchqcU+M2NhY7rzzTm666SZ+/OMf8+c//7nJMaZpsnTpUgYMGNDkMr/fz+OPP8706dP5xS9+Qe/evXnllVdaHGhxcTE7d+7kL3/5CwCvv/46H3/8MSkpKfzTP/0Tzz//PHPnzuWll17i1ltvDf9eIBBg+/btFBYWtvi2RFqiuLg40iGINKKcFDtSXordKCfFbpSTYkd2zssWFzFuuOGG8M+lpaX06tWr6ZU5HNx88824XK4ml23atInOnTuTkZEBwJAhQ1i5cmWLA928eTP9+/fnZz/7GQsXLmTGjBmMHDkSgPz8fP71X/+Vm266ibvuuouuXbuGfy8uLo5///d/b7S9RKQ1bN68OdIhiDSinBQ7Ul6K3SgnxW6Uk2JHds7Lk+qJsXv3bhYvXkxFRQX33nvvSd1QeXk5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/vu///ukxzRf15EjR3j55Zex2+0sXry41x9fBLY///nP/Otf/+LSSy/lRz/6EXq9nueffx5FUbj55psBePHFFzlw4AALFixg9OjRp3ysvtynbdmyhTfeeIM77rjjvPxeQB+EGKNHj2b69Ols2rSJRYsW+b//6aefYjabT/ozN954IyEhISd8f+zYsUyYMOG8basYnHJycmhsbKS2tpY333yT2267zX/b4cOHKSsrIz4+vlcCDIB58+bx9ttv98pjicHF7XazfPlynn76af/+79VXX6WioqJXDg576rrrruPdd9/ts+cTA8v5rlNFUfjBD37Agw8++LUfSwwuo0aNYt68eQAsXryYq6++mokTJ7Jjxw4qKyvJz8/vs22Jioriqquu4vXXX++z5xSB7XT1aTKZzstzpqWlMX36dPbt23deHl8EtoULF7Jr1y4uvvhi9HptgOLQoUO4XC5/iPHNb36TkSNHnjbAgL7dp11++eVs3779vD7HeQ8xTmbt2rVccsklvPnmm9x///18//vfZ+/evezZs4clS5bwj3/8g9zcXGbPno3H4+H555+nubmZqKgonE6nPwUtKSnh1VdfJTk5mYaGBiZNmsSkSZPYunUrL7zwAhMnTuSOO+5g165drFq1iuuuu47p06f3x0sWAeCaa65h5cqVXHfddf6zz5s3b2bGjBns2rULgJqaGl599VXsdju1tbVMnjyZyZMns2nTJv7+979z4YUXUlNTQ0lJCd/5znf41re+BcC+ffv4y1/+Qnx8PBkZGd2e95VXXqGtrQ2r1UpzczO33HILLpeLP/7xj9TW1nLzzTeTm5vLH/7wB1pbW7nzzjsJCwvr2zdH9Am3243D4aChoYG4uDgAf3i2d+9eNmzYQGJiIrW1tVxxxRWMGjWKQ4cO+euzpaWFCy+8kLy8PNatW8fatWupqKjghRdeIDIyksWLF/tr9aKLLqKqqor9+/dz2223ERcXx4svvkhUVBQJCQkce2Gqt99+m8LCQmw2G3V1ddx4443YbDYef/xxDh48yE033cT06dN56aWX+OKLL1iyZAkJCQl9/waKPnGqOvV6vTz99NN88cUXLFu2jEOHDrFixQr/3+vXXnuNzZs3M3PmTIqKiigrK2PevHn+kw/bt29n48aNJCYmntAB9PTTT2O1WtHr9Xi9XubPn09lZSVPPfUUXq+XhQsXEhcXx9KlS1EUhbvvvtv/gU4MHl0HiMebMmUK+/btY9euXaxYsYLDhw8TEhLCz3/+cwCeffbZM9bllVdeyZEjR9izZw8PPfQQn3zyCU1NTVgsFiorK7njjjswm828/fbbfPLJJyQmJmK1Wv3b4PV6eeyxx0hISMDlchEWFsbs2bP58ssvWbZsGTabjR/96EeAVs8ZGRnceuut5/9NE33mVPWZnJzMypUr/X+HP/jgA1599VX/WWiXy8Urr7yCoii0t7cTGRnJ9773PXw+HytWrMBo1A7HmpubueOOOwDts2NhYSFxcXHodDr/czU2NvLss8+SnJxMa2srGRkZXH755Xz22Wc899xzpKens3jxYiorK3n22We55JJLuOaaa87/myPOm5ycHHbt2sXYsWMpLi5mypQp/P3vf6eqqorY2Fjy8/O55pprWL58OWFhYTQ2NjJq1Cguu+wygFPu0850bHPkyBHWr19PZGQk1dXVzJo1i8zMTMrKynjttdeIj4+npqaG3Nxcpk2bRm1tLc8++yyhoaFERkbidrv9z7Vt2zb+85//EBsbS01NDddffz1JSUmsWLGCd999lx/+8IdcddVVvPXWW7z77rvcfvvtZGZmnvZ96bMQo6qqiieffBLQwodLLrmE2267jT179qAoCr/61a947733mDBhAsXFxf6f27JlC42NjfzqV78C4P777/eHGAaDgdmzZ5ORkYHH4+GnP/0pEydOZPr06Rw8eBC73Y5er2fEiBFkZ2dLgDHExcfHM2XKFH83xuHDh0lKSkJRFP99/vSnPzFr1iwmT56My+XiJz/5CSkpKcycOZPDhw/T2NjIz372M8rKyvi///s/vvWtb+F2u3nqqae46667GDFiBHv37u32vGlpaVx00UUA/POf/2TTpk1ce+21LF68mF/84hdkZWWh1+uJi4vjiiuukABjELNarXz3u9/l7rvvZvz48eTm5jJ58mRCQkL8rYExMTE0Nzdz//3388c//pERI0aQm5sLHB0nGTVqFOvWrQO0ur744ov9Z2m6arWmpoa77rqLgwcPYjabWbZsGddccw1Tp06lsrKyWxIfERHBlVdeiV6vZ8eOHbzxxhssXLiQJUuWcPvtt/tbAePi4pg8ebIEGIPc6ep09uzZ/g6KY2sT4Ac/+AEHDhxAVVV+/vOfs3v3bt544w0mTJhA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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "# Let's aggregate by Day of the Week\n", "df_weekday = df.groupby(['Day of Week']).sum()\n", @@ -1393,180 +434,9 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
ImpressionsSpent
meanstdmeanstd
Target GenderTarget Age
B20-25499999.5135142.068970225522.364865210082.169476
20-30499999.2337662.372519248960.376623247992.581816
20-35499999.6781611.742053280387.114943229581.623961
20-40499999.5662651.761324235929.072289206375.966516
20-45499999.3500002.323224265572.050000269311.696990
..................
M45-50499999.0000001.490712323302.200000231532.010005
45-55499999.1428571.956674344844.142857238843.833685
45-60499999.7500001.332785204209.750000161287.282792
45-65500000.1250001.356203249832.375000190022.680442
45-70500000.0000000.000000227598.50000013319.770437
\n", - "

90 rows × 4 columns

\n", - "
" - ], - "text/plain": [ - " Impressions Spent \\\n", - " mean std mean \n", - "Target Gender Target Age \n", - "B 20-25 499999.513514 2.068970 225522.364865 \n", - " 20-30 499999.233766 2.372519 248960.376623 \n", - " 20-35 499999.678161 1.742053 280387.114943 \n", - " 20-40 499999.566265 1.761324 235929.072289 \n", - " 20-45 499999.350000 2.323224 265572.050000 \n", - "... ... ... ... \n", - "M 45-50 499999.000000 1.490712 323302.200000 \n", - " 45-55 499999.142857 1.956674 344844.142857 \n", - " 45-60 499999.750000 1.332785 204209.750000 \n", - " 45-65 500000.125000 1.356203 249832.375000 \n", - " 45-70 500000.000000 0.000000 227598.500000 \n", - "\n", - " \n", - " std \n", - "Target Gender Target Age \n", - "B 20-25 210082.169476 \n", - " 20-30 247992.581816 \n", - " 20-35 229581.623961 \n", - " 20-40 206375.966516 \n", - " 20-45 269311.696990 \n", - "... ... \n", - "M 45-50 231532.010005 \n", - " 45-55 238843.833685 \n", - " 45-60 161287.282792 \n", - " 45-65 190022.680442 \n", - " 45-70 13319.770437 \n", - "\n", - "[90 rows x 4 columns]" - ] - }, - "execution_count": 31, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Let's aggregate by gender and age\n", "agg_config = {\n", @@ -1579,505 +449,11 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": null, "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
ClicksImpressionsSpent
Target GenderBFMBFMBFM
Target Age
20-25286652827361433752471369999643549997342499984166886551560955022026226
20-30337774134797753297034384999413999997839499933191699491781010815777111
20-35297834227277712981045434999723449997938499964243936791813297017653304
20-40331259032695493163736414999644049996437999981195821132231700319464410
20-45760654984872774256999998789999928499991531144134619475014206
25-30259944425489853497998354999703549998641999977160493791884410319496786
25-35357686840242683019695459999835499995937999944227707203159455519591946
25-40290705726463123511864364999663649997641999946211243881738228124660000
25-45262382328205362871440349999913549995232999963205495611992883616185794
25-50462439753028654451649999994999769499977340397537075413496496
30-35338842329947533490426439999833899995641499932261641571588528518907029
30-40324241537363803449705394999544449995942499939191816961897346518056334
30-45327545525383684259140369999673349998149999943201238972019285824024862
30-50285411629956293566316369999293699997042999951179256871582030021335822
30-5510419296078463873371149997874999973999986586001941433092250109
35-40314705535731293374785389999634149997941499969207069651729751418888529
35-45320139928744752412763399999453649996429999965200579671947853812951974
35-50307811833869873153942389999493949997442999947183248891850873220743245
35-55313940330860263845534389999813949995142000006199830512489940320362757
35-6029348111224664484153999983114999878000000181380363657025748788
40-45285260627926973101402339999743249997536499991159963111658483719899182
40-50318818429977153138284399999553799996039999983173351211658330123051858
40-55345035333293753543599394999184399998240999954212372902304357018746124
40-60334555629237193303711419999513599995239499956199525031756403819037412
40-65287297660921250325499998980000004499993252797338316622226148
45-50415484542915414364499999859999834999990209214045356433233022
45-556866407091739758728999983799999410499982551504437165587241727
45-60850162718080817504799999599999829999995366622166064854084195
45-65376761573359297859449999169999974000001183880125733601998659
45-707933911693442055150000415000021000000835152464627455197
\n", - "
" - ], - "text/plain": [ - " Clicks Impressions \\\n", - "Target Gender B F M B F M \n", - "Target Age \n", - "20-25 2866528 2736143 3752471 36999964 35499973 42499984 \n", - "20-30 3377741 3479775 3297034 38499941 39999978 39499933 \n", - "20-35 2978342 2727771 2981045 43499972 34499979 38499964 \n", - "20-40 3312590 3269549 3163736 41499964 40499964 37999981 \n", - "20-45 760654 984872 774256 9999987 8999992 8499991 \n", - "25-30 2599444 2548985 3497998 35499970 35499986 41999977 \n", - "25-35 3576868 4024268 3019695 45999983 54999959 37999944 \n", - "25-40 2907057 2646312 3511864 36499966 36499976 41999946 \n", - "25-45 2623823 2820536 2871440 34999991 35499952 32999963 \n", - "25-50 462439 753028 654451 6499999 9499976 9499977 \n", - "30-35 3388423 2994753 3490426 43999983 38999956 41499932 \n", - "30-40 3242415 3736380 3449705 39499954 44499959 42499939 \n", - "30-45 3275455 2538368 4259140 36999967 33499981 49999943 \n", - "30-50 2854116 2995629 3566316 36999929 36999970 42999951 \n", - "30-55 1041929 607846 387337 11499978 7499997 3999986 \n", - "35-40 3147055 3573129 3374785 38999963 41499979 41499969 \n", - "35-45 3201399 2874475 2412763 39999945 36499964 29999965 \n", - "35-50 3078118 3386987 3153942 38999949 39499974 42999947 \n", - "35-55 3139403 3086026 3845534 38999981 39499951 42000006 \n", - "35-60 293481 1122466 448415 3999983 11499987 8000000 \n", - "40-45 2852606 2792697 3101402 33999974 32499975 36499991 \n", - "40-50 3188184 2997715 3138284 39999955 37999960 39999983 \n", - "40-55 3450353 3329375 3543599 39499918 43999982 40999954 \n", - "40-60 3345556 2923719 3303711 41999951 35999952 39499956 \n", - "40-65 287297 660921 250325 4999989 8000000 4499993 \n", - "45-50 415484 542915 414364 4999998 5999983 4999990 \n", - "45-55 686640 709173 975872 8999983 7999994 10499982 \n", - "45-60 850162 718080 817504 7999995 9999982 9999995 \n", - "45-65 376761 573359 297859 4499991 6999997 4000001 \n", - "45-70 79339 116934 42055 1500004 1500002 1000000 \n", - "\n", - " Spent \n", - "Target Gender B F M \n", - "Target Age \n", - "20-25 16688655 15609550 22026226 \n", - "20-30 19169949 17810108 15777111 \n", - "20-35 24393679 18132970 17653304 \n", - "20-40 19582113 22317003 19464410 \n", - "20-45 5311441 3461947 5014206 \n", - "25-30 16049379 18844103 19496786 \n", - "25-35 22770720 31594555 19591946 \n", - "25-40 21124388 17382281 24660000 \n", - "25-45 20549561 19928836 16185794 \n", - "25-50 3403975 3707541 3496496 \n", - "30-35 26164157 15885285 18907029 \n", - "30-40 19181696 18973465 18056334 \n", - "30-45 20123897 20192858 24024862 \n", - "30-50 17925687 15820300 21335822 \n", - "30-55 5860019 4143309 2250109 \n", - "35-40 20706965 17297514 18888529 \n", - "35-45 20057967 19478538 12951974 \n", - "35-50 18324889 18508732 20743245 \n", - "35-55 19983051 24899403 20362757 \n", - "35-60 1813803 6365702 5748788 \n", - "40-45 15996311 16584837 19899182 \n", - "40-50 17335121 16583301 23051858 \n", - "40-55 21237290 23043570 18746124 \n", - "40-60 19952503 17564038 19037412 \n", - "40-65 2527973 3831662 2226148 \n", - "45-50 2092140 4535643 3233022 \n", - "45-55 5515044 3716558 7241727 \n", - "45-60 3666221 6606485 4084195 \n", - "45-65 1838801 2573360 1998659 \n", - "45-70 835152 464627 455197 " - ] - }, - "execution_count": 32, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# finally, let's make a pivot table\n", "df.pivot_table(\n", @@ -2105,7 +481,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.7" + "version": "3.11.2" }, "toc-autonumbering": false, "toc-showcode": false, diff --git a/ch13/example.ipynb b/ch13/example.ipynb index 988fa45..20c18f3 100644 --- a/ch13/example.ipynb +++ b/ch13/example.ipynb @@ -42,7 +42,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "1.35 µs ± 7.01 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)\n" + "1.85 µs ± 161 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)\n" ] } ], @@ -67,7 +67,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.6" + "version": "3.11.2" } }, "nbformat": 4, diff --git a/ch13/requirements/requirements.in b/ch13/requirements/requirements.in deleted file mode 100644 index 82bb026..0000000 --- a/ch13/requirements/requirements.in +++ /dev/null @@ -1,8 +0,0 @@ -arrow -faker -jupyter -jupyterlab -matplotlib -numpy -openpyxl -pandas diff --git a/ch13/requirements/requirements.txt b/ch13/requirements/requirements.txt deleted file mode 100644 index 7f9815b..0000000 --- a/ch13/requirements/requirements.txt +++ /dev/null @@ -1,292 +0,0 @@ -# -# This file is autogenerated by pip-compile with python 3.9 -# To update, run: -# -# pip-compile requirements.in -# -anyio==3.3.0 - # via jupyter-server -argon2-cffi==20.1.0 - # via - # jupyter-server - # notebook -arrow==1.1.1 - # via -r requirements.in -async-generator==1.10 - # via nbclient -attrs==21.2.0 - # via jsonschema -babel==2.9.1 - # via jupyterlab-server -backcall==0.2.0 - # via ipython -bleach==4.0.0 - # via nbconvert -certifi==2021.5.30 - # via requests -cffi==1.14.6 - # via argon2-cffi -charset-normalizer==2.0.4 - # via requests -cycler==0.10.0 - # via matplotlib -debugpy==1.4.1 - # via ipykernel -decorator==5.0.9 - # via ipython -defusedxml==0.7.1 - # via nbconvert -entrypoints==0.3 - # via - # jupyterlab-server - # nbconvert -et-xmlfile==1.1.0 - # via openpyxl -faker==8.11.0 - # via -r requirements.in -idna==3.2 - # via - # anyio - # requests -ipykernel==6.1.0 - # via - # ipywidgets - # jupyter - # jupyter-console - # notebook - # qtconsole -ipython==7.26.0 - # via - # ipykernel - # ipywidgets - # jupyter-console - # jupyterlab -ipython-genutils==0.2.0 - # via - # jupyter-server - # nbformat - # notebook - # qtconsole - # traitlets -ipywidgets==7.6.3 - # via jupyter -jedi==0.18.0 - # via ipython -jinja2==3.0.1 - # via - # jupyter-server - # jupyterlab - # jupyterlab-server - # nbconvert - # notebook -json5==0.9.6 - # via jupyterlab-server -jsonschema==3.2.0 - # via - # jupyterlab-server - # nbformat -jupyter==1.0.0 - # via -r requirements.in -jupyter-client==6.1.12 - # via - # ipykernel - # jupyter-console - # jupyter-server - # nbclient - # notebook - # qtconsole -jupyter-console==6.4.0 - # via jupyter -jupyter-core==4.7.1 - # via - # jupyter-client - # jupyter-server - # jupyterlab - # nbconvert - # nbformat - # notebook - # qtconsole -jupyter-server==1.10.2 - # via - # jupyterlab - # jupyterlab-server - # nbclassic -jupyterlab==3.1.6 - # via -r requirements.in -jupyterlab-pygments==0.1.2 - # via nbconvert -jupyterlab-server==2.7.0 - # via jupyterlab -jupyterlab-widgets==1.0.0 - # via ipywidgets -kiwisolver==1.3.1 - # via matplotlib -markupsafe==2.0.1 - # via jinja2 -matplotlib==3.4.3 - # via -r requirements.in -matplotlib-inline==0.1.2 - # via - # ipykernel - # ipython -mistune==0.8.4 - # via nbconvert -nbclassic==0.3.1 - # via jupyterlab -nbclient==0.5.3 - # via nbconvert -nbconvert==6.1.0 - # via - # jupyter - # jupyter-server - # notebook -nbformat==5.1.3 - # via - # ipywidgets - # jupyter-server - # nbclient - # nbconvert - # notebook -nest-asyncio==1.5.1 - # via nbclient -notebook==6.4.3 - # via - # jupyter - # nbclassic - # widgetsnbextension -numpy==1.21.1 - # via - # -r requirements.in - # matplotlib - # pandas -openpyxl==3.0.7 - # via -r requirements.in -packaging==21.0 - # via - # bleach - # jupyterlab - # jupyterlab-server -pandas==1.3.1 - # via -r requirements.in -pandocfilters==1.4.3 - # via nbconvert -parso==0.8.2 - # via jedi -pexpect==4.8.0 - # via ipython -pickleshare==0.7.5 - # via ipython -pillow==8.3.1 - # via matplotlib -prometheus-client==0.11.0 - # via - # jupyter-server - # notebook -prompt-toolkit==3.0.19 - # via - # ipython - # jupyter-console -ptyprocess==0.7.0 - # via - # pexpect - # terminado -pycparser==2.20 - # via cffi -pygments==2.9.0 - # via - # ipython - # jupyter-console - # jupyterlab-pygments - # nbconvert - # qtconsole -pyparsing==2.4.7 - # via - # matplotlib - # packaging -pyrsistent==0.18.0 - # via jsonschema -python-dateutil==2.8.2 - # via - # arrow - # faker - # jupyter-client - # matplotlib - # pandas -pytz==2021.1 - # via - # babel - # pandas -pyzmq==22.2.1 - # via - # jupyter-client - # jupyter-server - # notebook - # qtconsole -qtconsole==5.1.1 - # via jupyter -qtpy==1.9.0 - # via qtconsole -requests==2.26.0 - # via - # jupyterlab-server - # requests-unixsocket -requests-unixsocket==0.2.0 - # via jupyter-server -send2trash==1.8.0 - # via - # jupyter-server - # notebook -six==1.16.0 - # via - # argon2-cffi - # bleach - # cycler - # jsonschema - # python-dateutil -sniffio==1.2.0 - # via anyio -terminado==0.11.0 - # via - # jupyter-server - # notebook -testpath==0.5.0 - # via nbconvert -text-unidecode==1.3 - # via faker -tornado==6.1 - # via - # ipykernel - # jupyter-client - # jupyter-server - # jupyterlab - # notebook - # terminado -traitlets==5.0.5 - # via - # ipykernel - # ipython - # ipywidgets - # jupyter-client - # jupyter-core - # jupyter-server - # matplotlib-inline - # nbclient - # nbconvert - # nbformat - # notebook - # qtconsole -urllib3==1.26.6 - # via - # requests - # requests-unixsocket -wcwidth==0.2.5 - # via prompt-toolkit -webencodings==0.5.1 - # via bleach -websocket-client==1.2.1 - # via jupyter-server -widgetsnbextension==3.5.1 - # via ipywidgets - -# The following packages are considered to be unsafe in a requirements file: -# setuptools diff --git a/ch14/api_code/api/admin.py b/ch14/api_code/api/admin.py index 766d34f..a47ce82 100644 --- a/ch14/api_code/api/admin.py +++ b/ch14/api_code/api/admin.py @@ -19,9 +19,7 @@ def ensure_admin(settings: Settings, authorization: str): - if not is_admin( - settings=settings, authorization=authorization - ): + if not is_admin(settings=settings, authorization=authorization): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=f"You must be an admin to access this endpoint.", diff --git a/ch14/api_code/api/crud.py b/ch14/api_code/api/crud.py index 8ccdb93..e14ce88 100644 --- a/ch14/api_code/api/crud.py +++ b/ch14/api_code/api/crud.py @@ -17,24 +17,14 @@ def get_users(db: Session, email: str = None): def get_user(db: Session, user_id: int): - return ( - db.query(models.User) - .filter(models.User.id == user_id) - .first() - ) + return db.query(models.User).filter(models.User.id == user_id).first() def get_user_by_email(db: Session, email: str): - return ( - db.query(models.User) - .filter(models.User.email.ilike(email)) - .first() - ) + return db.query(models.User).filter(models.User.email.ilike(email)).first() -def create_user( - db: Session, user: schemas.UserCreate, user_id: int = None -): +def create_user(db: Session, user: schemas.UserCreate, user_id: int = None): hashed_password = models.User.hash_password(user.password) user_dict = { **user.dict(exclude_unset=True), @@ -49,21 +39,13 @@ def create_user( return db_user -def update_user( - db: Session, user: schemas.UserUpdate, user_id: int -): +def update_user(db: Session, user: schemas.UserUpdate, user_id: int): user_dict = { **user.dict(exclude_unset=True), } if user.password is not None: - user_dict.update( - {"password": models.User.hash_password(user.password)} - ) - stm = ( - update(models.User) - .where(models.User.id == user_id) - .values(user_dict) - ) + user_dict.update({"password": models.User.hash_password(user.password)}) + stm = update(models.User).where(models.User.id == user_id).values(user_dict) result = db.execute(stm) db.commit() return result.rowcount @@ -87,19 +69,11 @@ def get_stations(db: Session, code: str = None): def get_station(db: Session, station_id: int): - return ( - db.query(models.Station) - .filter(models.Station.id == station_id) - .first() - ) + return db.query(models.Station).filter(models.Station.id == station_id).first() def get_station_by_code(db: Session, code: str): - return ( - db.query(models.Station) - .filter(models.Station.code.ilike(code)) - .first() - ) + return db.query(models.Station).filter(models.Station.code.ilike(code)).first() def create_station( @@ -113,9 +87,7 @@ def create_station( return db_station -def update_station( - db: Session, station: schemas.StationUpdate, station_id: int -): +def update_station(db: Session, station: schemas.StationUpdate, station_id: int): stm = ( update(models.Station) .where(models.Station.id == station_id) @@ -127,9 +99,7 @@ def update_station( def delete_station(db: Session, station_id: int): - stm = delete(models.Station).where( - models.Station.id == station_id - ) + stm = delete(models.Station).where(models.Station.id == station_id) result = db.execute(stm) db.commit() return result.rowcount @@ -164,19 +134,11 @@ def get_trains( def get_train(db: Session, train_id: int): - return ( - db.query(models.Train) - .filter(models.Train.id == train_id) - .first() - ) + return db.query(models.Train).filter(models.Train.id == train_id).first() def get_train_by_name(db: Session, name: str): - return ( - db.query(models.Train) - .filter(models.Train.name == name) - .first() - ) + return db.query(models.Train).filter(models.Train.name == name).first() def create_train(db: Session, train: schemas.TrainCreate): @@ -203,18 +165,12 @@ def get_tickets(db: Session): def get_ticket(db: Session, ticket_id: int): - return ( - db.query(models.Ticket) - .filter(models.Ticket.id == ticket_id) - .first() - ) + return db.query(models.Ticket).filter(models.Ticket.id == ticket_id).first() def create_ticket(db: Session, ticket: schemas.TicketCreate): ticket_dict = ticket.dict(exclude_unset=True) - ticket_dict.update( - {"created_at": datetime.now(tz=timezone.utc)} - ) + ticket_dict.update({"created_at": datetime.now(tz=timezone.utc)}) db_ticket = models.Ticket(**ticket_dict) db.add(db_ticket) db.commit() @@ -223,9 +179,7 @@ def create_ticket(db: Session, ticket: schemas.TicketCreate): def delete_ticket(db: Session, ticket_id: int): - stm = delete(models.Ticket).where( - models.Ticket.id == ticket_id - ) + stm = delete(models.Ticket).where(models.Ticket.id == ticket_id) result = db.execute(stm) db.commit() return result.rowcount diff --git a/ch14/api_code/api/database.py b/ch14/api_code/api/database.py index a806331..1556ec7 100644 --- a/ch14/api_code/api/database.py +++ b/ch14/api_code/api/database.py @@ -17,8 +17,6 @@ echo=settings.debug, # when debug is True, queries are logged ) -SessionLocal = sessionmaker( - autocommit=False, autoflush=False, bind=engine -) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base() diff --git a/ch14/api_code/api/models.py b/ch14/api_code/api/models.py index 5584820..7175300 100644 --- a/ch14/api_code/api/models.py +++ b/ch14/api_code/api/models.py @@ -40,9 +40,7 @@ class Station(Base): __tablename__ = "station" id = Column(Integer, primary_key=True) - code = Column( - Unicode(UNICODE_LEN), nullable=False, unique=True - ) + code = Column(Unicode(UNICODE_LEN), nullable=False, unique=True) country = Column(Unicode(UNICODE_LEN), nullable=False) city = Column(Unicode(UNICODE_LEN), nullable=False) @@ -69,18 +67,14 @@ class Train(Base): id = Column(Integer, primary_key=True) name = Column(Unicode(UNICODE_LEN), nullable=False) - station_from_id = Column( - ForeignKey("station.id"), nullable=False - ) + station_from_id = Column(ForeignKey("station.id"), nullable=False) station_from = relationship( "Station", foreign_keys=[station_from_id], back_populates="departures", ) - station_to_id = Column( - ForeignKey("station.id"), nullable=False - ) + station_to_id = Column(ForeignKey("station.id"), nullable=False) station_to = relationship( "Station", foreign_keys=[station_to_id], @@ -108,22 +102,16 @@ class Ticket(Base): id = Column(Integer, primary_key=True) created_at = Column(DateTime(timezone=True), nullable=False) user_id = Column(ForeignKey("user.id"), nullable=False) - user = relationship( - "User", foreign_keys=[user_id], back_populates="tickets" - ) + user = relationship("User", foreign_keys=[user_id], back_populates="tickets") train_id = Column(ForeignKey("train.id"), nullable=False) - train = relationship( - "Train", foreign_keys=[train_id], back_populates="tickets" - ) + train = relationship("Train", foreign_keys=[train_id], back_populates="tickets") price = Column(Float, default=0, nullable=False) car_class = Column(Enum(Classes), nullable=False) def __repr__(self): - return ( - f"" - ) + return f"" __str__ = __repr__ @@ -143,12 +131,8 @@ class User(Base): def is_valid_password(self, password: str): """Tell if password matches the one stored in DB.""" - salt, stored_hash = self.password.split( - self.pwd_separator - ) - _, computed_hash = _hash( - password=password, salt=bytes.fromhex(salt) - ) + salt, stored_hash = self.password.split(self.pwd_separator) + _, computed_hash = _hash(password=password, salt=bytes.fromhex(salt)) return secrets.compare_digest(stored_hash, computed_hash) @classmethod @@ -157,10 +141,7 @@ def hash_password(cls, password: str, salt: bytes = None): return f"{salt}{cls.pwd_separator}{hashed}" def __repr__(self): - return ( - f"<{self.full_name}: id={self.id} " - f"role={self.role.name}>" - ) + return f"<{self.full_name}: id={self.id} " f"role={self.role.name}>" __str__ = __repr__ diff --git a/ch14/api_code/api/stations.py b/ch14/api_code/api/stations.py index 8d83854..6c11ea1 100644 --- a/ch14/api_code/api/stations.py +++ b/ch14/api_code/api/stations.py @@ -18,15 +18,11 @@ @router.get("", response_model=list[Station], tags=["Stations"]) -def get_stations( - db: Session = Depends(get_db), code: Optional[str] = None -): +def get_stations(db: Session = Depends(get_db), code: Optional[str] = None): return crud.get_stations(db=db, code=code) -@router.get( - "/{station_id}", response_model=Station, tags=["Stations"] -) +@router.get("/{station_id}", response_model=Station, tags=["Stations"]) def get_station(station_id: int, db: Session = Depends(get_db)): db_station = crud.get_station(db=db, station_id=station_id) if db_station is None: @@ -42,9 +38,7 @@ def get_station(station_id: int, db: Session = Depends(get_db)): response_model=list[Train], tags=["Trains"], ) -def get_station_departures( - station_id: int, db: Session = Depends(get_db) -): +def get_station_departures(station_id: int, db: Session = Depends(get_db)): db_station = _get_station(db=db, station_id=station_id) return db_station.departures @@ -64,9 +58,7 @@ def _get_station(db: Session, station_id: int): response_model=list[Train], tags=["Trains"], ) -def get_station_arrivals( - station_id: int, db: Session = Depends(get_db) -): +def get_station_arrivals(station_id: int, db: Session = Depends(get_db)): db_station = _get_station(db=db, station_id=station_id) return db_station.arrivals @@ -77,12 +69,8 @@ def get_station_arrivals( status_code=status.HTTP_201_CREATED, tags=["Stations"], ) -def create_station( - station: StationCreate, db: Session = Depends(get_db) -): - db_station = crud.get_station_by_code( - db=db, code=station.code - ) +def create_station(station: StationCreate, db: Session = Depends(get_db)): + db_station = crud.get_station_by_code(db=db, code=station.code) if db_station: raise HTTPException( status_code=400, @@ -106,16 +94,12 @@ def update_station( ) else: - crud.update_station( - db=db, station=station, station_id=station_id - ) + crud.update_station(db=db, station=station, station_id=station_id) return Response(status_code=status.HTTP_204_NO_CONTENT) @router.delete("/{station_id}", tags=["Stations"]) -def delete_station( - station_id: int, db: Session = Depends(get_db) -): +def delete_station(station_id: int, db: Session = Depends(get_db)): row_count = crud.delete_station(db=db, station_id=station_id) if row_count: return Response(status_code=status.HTTP_204_NO_CONTENT) diff --git a/ch14/api_code/api/tickets.py b/ch14/api_code/api/tickets.py index 39f6ac5..2061ae7 100644 --- a/ch14/api_code/api/tickets.py +++ b/ch14/api_code/api/tickets.py @@ -20,9 +20,7 @@ def get_tickets(db: Session = Depends(get_db)): return crud.get_tickets(db=db) -@router.get( - "/{ticket_id}", response_model=Ticket, tags=["Tickets"] -) +@router.get("/{ticket_id}", response_model=Ticket, tags=["Tickets"]) def get_ticket(ticket_id: int, db: Session = Depends(get_db)): db_ticket = crud.get_ticket(db=db, ticket_id=ticket_id) if db_ticket is None: @@ -39,9 +37,7 @@ def get_ticket(ticket_id: int, db: Session = Depends(get_db)): status_code=status.HTTP_201_CREATED, tags=["Tickets"], ) -def create_ticket( - ticket: TicketCreate, db: Session = Depends(get_db) -): +def create_ticket(ticket: TicketCreate, db: Session = Depends(get_db)): return crud.create_ticket(db=db, ticket=ticket) diff --git a/ch14/api_code/api/trains.py b/ch14/api_code/api/trains.py index 9f5aa3b..2365718 100644 --- a/ch14/api_code/api/trains.py +++ b/ch14/api_code/api/trains.py @@ -36,9 +36,7 @@ def get_trains( def get_train(train_id: int, db: Session = Depends(get_db)): db_train = crud.get_train(db=db, train_id=train_id) if db_train is None: - raise HTTPException( - status_code=404, detail=f"Train {train_id} not found." - ) + raise HTTPException(status_code=404, detail=f"Train {train_id} not found.") return db_train @@ -47,14 +45,10 @@ def get_train(train_id: int, db: Session = Depends(get_db)): response_model=list[Ticket], tags=["Tickets"], ) -def get_train_tickets( - train_id: int, db: Session = Depends(get_db) -): +def get_train_tickets(train_id: int, db: Session = Depends(get_db)): db_train = crud.get_train(db=db, train_id=train_id) if db_train is None: - raise HTTPException( - status_code=404, detail=f"Train {train_id} not found." - ) + raise HTTPException(status_code=404, detail=f"Train {train_id} not found.") return db_train.tickets @@ -64,9 +58,7 @@ def get_train_tickets( status_code=status.HTTP_201_CREATED, tags=["Trains"], ) -def create_train( - train: TrainCreate, db: Session = Depends(get_db) -): +def create_train(train: TrainCreate, db: Session = Depends(get_db)): db_train = crud.get_train_by_name(db=db, name=train.name) if db_train: raise HTTPException( diff --git a/ch14/api_code/api/users.py b/ch14/api_code/api/users.py index 980933a..b15984d 100644 --- a/ch14/api_code/api/users.py +++ b/ch14/api_code/api/users.py @@ -26,9 +26,7 @@ @router.get("", response_model=list[User], tags=["Users"]) -def get_users( - db: Session = Depends(get_db), email: Optional[str] = None -): +def get_users(db: Session = Depends(get_db), email: Optional[str] = None): return crud.get_users(db=db, email=email) @@ -75,9 +73,7 @@ def create_user(user: UserCreate, db: Session = Depends(get_db)): @router.put("/{user_id}", response_model=User, tags=["Users"]) -def update_user( - user_id: int, user: UserUpdate, db: Session = Depends(get_db) -): +def update_user(user_id: int, user: UserUpdate, db: Session = Depends(get_db)): db_user = crud.get_user(db=db, user_id=user_id) if db_user is None: diff --git a/ch14/api_code/api/util.py b/ch14/api_code/api/util.py index 6863040..278d077 100644 --- a/ch14/api_code/api/util.py +++ b/ch14/api_code/api/util.py @@ -31,19 +31,13 @@ def extract_payload(token: str, key: str): raise InvalidToken(str(err)) -def is_admin( - settings: Settings, authorization: Optional[str] = None -): +def is_admin(settings: Settings, authorization: Optional[str] = None): if authorization is None: return False - partition_key = ( - "Bearer" if "Bearer" in authorization else "bearer" - ) + partition_key = "Bearer" if "Bearer" in authorization else "bearer" - *dontcare, token = authorization.partition( - f"{partition_key} " - ) + *dontcare, token = authorization.partition(f"{partition_key} ") token = token.strip() try: diff --git a/ch14/api_code/dummy_data.py b/ch14/api_code/dummy_data.py index 88e2bfb..c4b01c6 100644 --- a/ch14/api_code/dummy_data.py +++ b/ch14/api_code/dummy_data.py @@ -29,7 +29,6 @@ def new_db(filename): if __name__ == "__main__": - new_db("train.db") Session = sessionmaker(bind=engine) @@ -76,33 +75,19 @@ def new_db(filename): Station(id=1, code="PAR", country="France", city="Paris"), Station(id=2, code="LDN", country="UK", city="London"), Station(id=3, code="KYV", country="Ukraine", city="Kyiv"), - Station( - id=4, code="STK", country="Sweden", city="Stockholm" - ), - Station( - id=5, code="WSW", country="Poland", city="Warsaw" - ), - Station( - id=6, code="MSK", country="Russia", city="Moskow" - ), + Station(id=4, code="STK", country="Sweden", city="Stockholm"), + Station(id=5, code="WSW", country="Poland", city="Warsaw"), + Station(id=6, code="MSK", country="Russia", city="Moskow"), Station( id=7, code="AMD", country="Netherlands", city="Amsterdam", ), - Station( - id=8, code="EDB", country="Scotland", city="Edinburgh" - ), - Station( - id=9, code="BDP", country="Hungary", city="Budapest" - ), - Station( - id=10, code="BCR", country="Romania", city="Bucharest" - ), - Station( - id=11, code="SFA", country="Bulgaria", city="Sofia" - ), + Station(id=8, code="EDB", country="Scotland", city="Edinburgh"), + Station(id=9, code="BDP", country="Hungary", city="Budapest"), + Station(id=10, code="BCR", country="Romania", city="Bucharest"), + Station(id=11, code="SFA", country="Bulgaria", city="Sofia"), ] session.bulk_save_objects(stations) @@ -123,12 +108,8 @@ def new_db(filename): station_to = choice(station_ids) name = f"{stations[station_from].city} -> {stations[station_to].city}" - departure = now + timedelta( - seconds=randint(-TEN_DAYS, TEN_DAYS) - ) - arrival = departure + timedelta( - seconds=randint(HOUR, DAY) - ) + departure = now + timedelta(seconds=randint(-TEN_DAYS, TEN_DAYS)) + arrival = departure + timedelta(seconds=randint(HOUR, DAY)) trains.append( Train( @@ -156,21 +137,15 @@ def new_db(filename): for ticket_id in range(NUM_TICKETS): price = round( - float(randint(MIN_PRICE, MAX_PRICE)) - + randint(0, 1) * random(), + float(randint(MIN_PRICE, MAX_PRICE)) + randint(0, 1) * random(), 2, ) tickets.append( Ticket( id=ticket_id, - created_at=now - + timedelta(seconds=randint(-TEN_DAYS, -HOUR)), - user_id=choice( - range(len(users) - 1) - ), # last user has no tickets - train_id=choice( - range(len(trains) - 1) - ), # last train has no tickets + created_at=now + timedelta(seconds=randint(-TEN_DAYS, -HOUR)), + user_id=choice(range(len(users) - 1)), # last user has no tickets + train_id=choice(range(len(trains) - 1)), # last train has no tickets price=price, car_class=choice(classes), ) diff --git a/ch14/api_code/main.py b/ch14/api_code/main.py index 154f03e..3ca485c 100644 --- a/ch14/api_code/main.py +++ b/ch14/api_code/main.py @@ -15,6 +15,4 @@ @app.get("/") def root(): - return { - "message": f"Welcome to version {settings.api_version} of our API" - } + return {"message": f"Welcome to version {settings.api_version} of our API"} diff --git a/ch14/apic/apic/settings.py b/ch14/apic/apic/settings.py index 4a0db1f..81a2d97 100644 --- a/ch14/apic/apic/settings.py +++ b/ch14/apic/apic/settings.py @@ -21,7 +21,8 @@ # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = "django-insecure-98u5ntukr@mo0e5c*ve+8bk5$i3lr+4n4gc^@=b-7c*j_lyxa(" +SECRET_KEY = "django-insecure-98u5ntukr@mo0e5c*ve+8bk5$i3lr+4n4gc^@=b-7c*\ + j_lyxa(" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -88,16 +89,20 @@ AUTH_PASSWORD_VALIDATORS = [ { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + "NAME": "django.contrib.auth.password_validation.\ + UserAttributeSimilarityValidator", }, { - "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + "NAME": "django.contrib.auth.password_validation.\ + MinimumLengthValidator", }, { - "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + "NAME": "django.contrib.auth.password_validation.\ + CommonPasswordValidator", }, { - "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + "NAME": "django.contrib.auth.password_validation.\ + NumericPasswordValidator", }, ] diff --git a/ch14/apic/manage.py b/ch14/apic/manage.py index 840e1fc..553dd78 100644 --- a/ch14/apic/manage.py +++ b/ch14/apic/manage.py @@ -7,9 +7,7 @@ def main(): """Run administrative tasks.""" - os.environ.setdefault( - "DJANGO_SETTINGS_MODULE", "apic.settings" - ) + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "apic.settings") try: from django.core.management import ( execute_from_command_line, diff --git a/ch14/apic/rails/forms.py b/ch14/apic/rails/forms.py index 4e93025..b9c52b2 100644 --- a/ch14/apic/rails/forms.py +++ b/ch14/apic/rails/forms.py @@ -4,6 +4,4 @@ class AuthenticateForm(forms.Form): email = forms.EmailField(max_length=256, label="Username") - password = forms.CharField( - label="Password", widget=forms.PasswordInput - ) + password = forms.CharField(label="Password", widget=forms.PasswordInput) diff --git a/ch14/apic/rails/urls.py b/ch14/apic/rails/urls.py index bfcd96d..87b981f 100644 --- a/ch14/apic/rails/urls.py +++ b/ch14/apic/rails/urls.py @@ -5,9 +5,7 @@ urlpatterns = [ path("", views.IndexView.as_view(), name="index"), - path( - "stations", views.StationsView.as_view(), name="stations" - ), + path("stations", views.StationsView.as_view(), name="stations"), path( "stations//departures", views.DeparturesView.as_view(), diff --git a/ch14/apic/rails/views.py b/ch14/apic/rails/views.py index dfdb2ab..c3f6fed 100644 --- a/ch14/apic/rails/views.py +++ b/ch14/apic/rails/views.py @@ -144,9 +144,7 @@ class AuthenticateResultView(generic.TemplateView): def get(self, request, *args, **kwargs): context = self.get_context_data(**kwargs) context["token"] = request.session.pop("token", None) - context["auth_error"] = request.session.pop( - "auth_error", None - ) + context["auth_error"] = request.session.pop("auth_error", None) return self.render_to_response(context) @@ -160,10 +158,6 @@ def prepare_trains(trains: list[dict], key: str): def parse_datetimes(train: dict): - train["arrives_at"] = datetime.fromisoformat( - train["arrives_at"] - ) - train["departs_at"] = datetime.fromisoformat( - train["departs_at"] - ) + train["arrives_at"] = datetime.fromisoformat(train["arrives_at"]) + train["departs_at"] = datetime.fromisoformat(train["departs_at"]) return train diff --git a/ch14/requirements/dev.in b/ch14/requirements/dev.in deleted file mode 100644 index 215984e..0000000 --- a/ch14/requirements/dev.in +++ /dev/null @@ -1,5 +0,0 @@ -jupyterlab -pdbpp -faker -isort -black diff --git a/ch14/requirements/dev.txt b/ch14/requirements/dev.txt deleted file mode 100644 index 5b75b07..0000000 --- a/ch14/requirements/dev.txt +++ /dev/null @@ -1,256 +0,0 @@ -# -# This file is autogenerated by pip-compile with python 3.9 -# To update, run: -# -# pip-compile dev.in -# -anyio==3.3.0 - # via jupyter-server -appdirs==1.4.4 - # via black -appnope==0.1.2 - # via - # ipykernel - # ipython -argon2-cffi==20.1.0 - # via - # jupyter-server - # notebook -attrs==21.2.0 - # via jsonschema -babel==2.9.1 - # via jupyterlab-server -backcall==0.2.0 - # via ipython -black==21.7b0 - # via -r dev.in -bleach==4.0.0 - # via nbconvert -certifi==2021.5.30 - # via requests -cffi==1.14.6 - # via argon2-cffi -charset-normalizer==2.0.4 - # via requests -click==8.0.1 - # via black -debugpy==1.4.1 - # via ipykernel -decorator==5.0.9 - # via ipython -defusedxml==0.7.1 - # via nbconvert -entrypoints==0.3 - # via - # jupyter-client - # jupyterlab-server - # nbconvert -faker==8.12.0 - # via -r dev.in -fancycompleter==0.9.1 - # via pdbpp -idna==3.2 - # via - # anyio - # requests -ipykernel==6.2.0 - # via notebook -ipython==7.26.0 - # via - # ipykernel - # jupyterlab -ipython-genutils==0.2.0 - # via - # jupyter-server - # nbformat - # notebook - # traitlets -isort==5.9.3 - # via -r dev.in -jedi==0.18.0 - # via ipython -jinja2==3.0.1 - # via - # jupyter-server - # jupyterlab - # jupyterlab-server - # nbconvert - # notebook -json5==0.9.6 - # via jupyterlab-server -jsonschema==3.2.0 - # via - # jupyterlab-server - # nbformat -jupyter-client==7.0.1 - # via - # ipykernel - # jupyter-server - # nbclient - # notebook -jupyter-core==4.7.1 - # via - # jupyter-client - # jupyter-server - # jupyterlab - # nbconvert - # nbformat - # notebook -jupyter-server==1.10.2 - # via - # jupyterlab - # jupyterlab-server - # nbclassic -jupyterlab==3.1.7 - # via -r dev.in -jupyterlab-pygments==0.1.2 - # via nbconvert -jupyterlab-server==2.7.1 - # via jupyterlab -markupsafe==2.0.1 - # via jinja2 -matplotlib-inline==0.1.2 - # via - # ipykernel - # ipython -mistune==0.8.4 - # via nbconvert -mypy-extensions==0.4.3 - # via black -nbclassic==0.3.1 - # via jupyterlab -nbclient==0.5.4 - # via nbconvert -nbconvert==6.1.0 - # via - # jupyter-server - # notebook -nbformat==5.1.3 - # via - # jupyter-server - # nbclient - # nbconvert - # notebook -nest-asyncio==1.5.1 - # via - # jupyter-client - # nbclient -notebook==6.4.3 - # via nbclassic -packaging==21.0 - # via - # bleach - # jupyterlab - # jupyterlab-server -pandocfilters==1.4.3 - # via nbconvert -parso==0.8.2 - # via jedi -pathspec==0.9.0 - # via black -pdbpp==0.10.3 - # via -r dev.in -pexpect==4.8.0 - # via ipython -pickleshare==0.7.5 - # via ipython -prometheus-client==0.11.0 - # via - # jupyter-server - # notebook -prompt-toolkit==3.0.20 - # via ipython -ptyprocess==0.7.0 - # via - # pexpect - # terminado -pycparser==2.20 - # via cffi -pygments==2.10.0 - # via - # ipython - # jupyterlab-pygments - # nbconvert - # pdbpp -pyparsing==2.4.7 - # via packaging -pyrepl==0.9.0 - # via fancycompleter -pyrsistent==0.18.0 - # via jsonschema -python-dateutil==2.8.2 - # via - # faker - # jupyter-client -pytz==2021.1 - # via babel -pyzmq==22.2.1 - # via - # jupyter-client - # jupyter-server - # notebook -regex==2021.8.3 - # via black -requests==2.26.0 - # via - # jupyterlab-server - # requests-unixsocket -requests-unixsocket==0.2.0 - # via jupyter-server -send2trash==1.8.0 - # via - # jupyter-server - # notebook -six==1.16.0 - # via - # argon2-cffi - # bleach - # jsonschema - # python-dateutil -sniffio==1.2.0 - # via anyio -terminado==0.11.1 - # via - # jupyter-server - # notebook -testpath==0.5.0 - # via nbconvert -text-unidecode==1.3 - # via faker -tomli==1.2.1 - # via black -tornado==6.1 - # via - # ipykernel - # jupyter-client - # jupyter-server - # jupyterlab - # notebook - # terminado -traitlets==5.0.5 - # via - # ipykernel - # ipython - # jupyter-client - # jupyter-core - # jupyter-server - # matplotlib-inline - # nbclient - # nbconvert - # nbformat - # notebook -urllib3==1.26.6 - # via - # requests - # requests-unixsocket -wcwidth==0.2.5 - # via prompt-toolkit -webencodings==0.5.1 - # via bleach -websocket-client==1.2.1 - # via jupyter-server -wmctrl==0.4 - # via pdbpp - -# The following packages are considered to be unsafe in a requirements file: -# setuptools diff --git a/ch14/requirements/requirements.in b/ch14/requirements/requirements.in deleted file mode 100644 index 526aee1..0000000 --- a/ch14/requirements/requirements.in +++ /dev/null @@ -1,8 +0,0 @@ -django -fastapi -pydantic[email] -pyjwt[crypto] -python-dotenv -requests -sqlalchemy -uvicorn diff --git a/ch14/requirements/requirements.txt b/ch14/requirements/requirements.txt deleted file mode 100644 index c4b67a9..0000000 --- a/ch14/requirements/requirements.txt +++ /dev/null @@ -1,62 +0,0 @@ -# -# This file is autogenerated by pip-compile with python 3.9 -# To update, run: -# -# pip-compile requirements.in -# -asgiref==3.4.1 - # via - # django - # uvicorn -certifi==2021.5.30 - # via requests -cffi==1.14.6 - # via cryptography -charset-normalizer==2.0.4 - # via requests -click==8.0.1 - # via uvicorn -cryptography==3.4.7 - # via pyjwt -django==3.2.6 - # via -r requirements.in -dnspython==2.1.0 - # via email-validator -email-validator==1.1.3 - # via pydantic -fastapi==0.68.0 - # via -r requirements.in -greenlet==1.1.1 - # via sqlalchemy -h11==0.12.0 - # via uvicorn -idna==3.2 - # via - # email-validator - # requests -pycparser==2.20 - # via cffi -pydantic[email]==1.8.2 - # via - # -r requirements.in - # fastapi -pyjwt[crypto]==2.1.0 - # via -r requirements.in -python-dotenv==0.19.0 - # via -r requirements.in -pytz==2021.1 - # via django -requests==2.26.0 - # via -r requirements.in -sqlalchemy==1.4.23 - # via -r requirements.in -sqlparse==0.4.1 - # via django -starlette==0.14.2 - # via fastapi -typing-extensions==3.10.0.0 - # via pydantic -urllib3==1.26.6 - # via requests -uvicorn==0.15.0 - # via -r requirements.in diff --git a/ch14/samples/typing.examples.py b/ch14/samples/typing.examples.py index f7c6cbb..d13c40c 100644 --- a/ch14/samples/typing.examples.py +++ b/ch14/samples/typing.examples.py @@ -11,7 +11,7 @@ b = "7" a + b == 14 -concatenate(a, b) == "77" +# concatenate(a, b) == "77" # dynamic typing @@ -22,56 +22,57 @@ # other languages -String greeting = "Hello"; -int m = 7; -float pi = 3.141592; +# String greeting = "Hello"; +# int m = 7; +# float pi = 3.141592; # function annotations def greet(first_name, last_name, age): return f"Greeting {first_name} {last_name} of age {age}" -def greet( - first_name: "First name of the person we are greeting", - last_name: "Last name of the person we are greeting", - age: "The person's age" -) -> "Returns the greeting sentence": - return f"Greeting {first_name} {last_name} of age {age}" -def greet(first_name: str, last_name: str, age: int = 18) -> str: - return f"Greeting {first_name} {last_name} of age {age}" +# def greet( +# first_name: "First name of the person we are greeting", +# last_name: "Last name of the person we are greeting", +# age: "The person's age" +# ) -> "Returns the greeting sentence": +# return f"Greeting {first_name} {last_name} of age {age}" + +# def greet(first_name: str, last_name: str, age: int = 18) -> str: +# return f"Greeting {first_name} {last_name} of age {age}" -# list -from typing import List +# # list +# from typing import List -def process_words(words: List[str]): - for word in words: - # do something with word +# def process_words(words: List[str]): +# for word in words: +# # do something with word -# dict -from typing import Dict +# # dict +# from typing import Dict -def process_users(users: Dict[str, int]): - for name, age in users.items(): - # do something with name and age +# def process_users(users: Dict[str, int]): +# for name, age in users.items(): +# # do something with name and age -# optional -from typing import Optional +# # optional +# from typing import Optional -def greet_again(name: Optional[str] = None): - if name is not None: - print(f"Hello {name}!") - else: - print("Hey dude") +# def greet_again(name: Optional[str] = None): +# if name is not None: +# print(f"Hello {name}!") +# else: +# print("Hey dude") -# custom -class Cat: - def __init__(self, name: str): - self.name = name +# # custom +# class Cat: +# def __init__(self, name: str): +# self.name = name -def call_cat(cat: Cat): - return f"{cat.name}! Come here!" +# def call_cat(cat: Cat): +# return f"{cat.name}! Come here!" diff --git a/ch15/requirements/build.in b/ch15/requirements/build.in deleted file mode 100644 index e47b6e9..0000000 --- a/ch15/requirements/build.in +++ /dev/null @@ -1,2 +0,0 @@ -build -twine diff --git a/ch15/requirements/build.txt b/ch15/requirements/build.txt deleted file mode 100644 index 51daf57..0000000 --- a/ch15/requirements/build.txt +++ /dev/null @@ -1,78 +0,0 @@ -# -# This file is autogenerated by pip-compile with python 3.9 -# To update, run: -# -# pip-compile build.in -# -bleach==4.1.0 - # via readme-renderer -build==0.6.0.post1 - # via -r build.in -certifi==2021.5.30 - # via requests -cffi==1.14.6 - # via cryptography -charset-normalizer==2.0.5 - # via requests -colorama==0.4.4 - # via twine -cryptography==3.4.8 - # via secretstorage -docutils==0.17.1 - # via readme-renderer -idna==3.2 - # via requests -importlib-metadata==4.8.1 - # via - # keyring - # twine -jeepney==0.7.1 - # via - # keyring - # secretstorage -keyring==23.2.1 - # via twine -packaging==21.0 - # via - # bleach - # build -pep517==0.11.0 - # via build -pkginfo==1.7.1 - # via twine -pycparser==2.20 - # via cffi -pygments==2.10.0 - # via readme-renderer -pyparsing==2.4.7 - # via packaging -readme-renderer==29.0 - # via twine -requests==2.26.0 - # via - # requests-toolbelt - # twine -requests-toolbelt==0.9.1 - # via twine -rfc3986==1.5.0 - # via twine -secretstorage==3.3.1 - # via keyring -six==1.16.0 - # via - # bleach - # readme-renderer -tomli==1.2.1 - # via - # build - # pep517 -tqdm==4.62.2 - # via twine -twine==3.4.2 - # via -r build.in -urllib3==1.26.6 - # via requests -webencodings==0.5.1 - # via bleach -zipp==3.5.0 - # via importlib-metadata diff --git a/ch15/requirements/main.in b/ch15/requirements/main.in deleted file mode 100644 index 9bf600e..0000000 --- a/ch15/requirements/main.in +++ /dev/null @@ -1,3 +0,0 @@ -platformdirs>=2.0 -pydantic>=1.8.2,<2.0 -requests~=2.0 diff --git a/ch15/requirements/main.txt b/ch15/requirements/main.txt deleted file mode 100644 index 986b5e4..0000000 --- a/ch15/requirements/main.txt +++ /dev/null @@ -1,22 +0,0 @@ -# -# This file is autogenerated by pip-compile with python 3.9 -# To update, run: -# -# pip-compile main.in -# -certifi==2021.5.30 - # via requests -charset-normalizer==2.0.5 - # via requests -idna==3.2 - # via requests -platformdirs==2.3.0 - # via -r main.in -pydantic==1.8.2 - # via -r main.in -requests==2.26.0 - # via -r main.in -typing-extensions==3.10.0.2 - # via pydantic -urllib3==1.26.6 - # via requests diff --git a/ch15/train-project/train_schedule/api/__init__.py b/ch15/train-project/train_schedule/api/__init__.py index 47bf702..fbc95eb 100644 --- a/ch15/train-project/train_schedule/api/__init__.py +++ b/ch15/train-project/train_schedule/api/__init__.py @@ -37,17 +37,13 @@ def get_arrivals(self, station_id): station""" url = self._make_url(https://github.com/self.STATIONS_PATH) - url = self._make_url( - self.STATION_ARRIVALS_PATH, station_id=station_id - ) + url = self._make_url(https://github.com/self.STATION_ARRIVALS_PATH,%20station_id=station_id) return TrainList.parse_obj(self._get(url)) def get_departures(self, station_id): """Get a list of trains departing from a particular station""" - url = self._make_url( - self.STATION_DEPARTURES_PATH, station_id=station_id - ) + url = self._make_url(https://github.com/self.STATION_DEPARTURES_PATH,%20station_id=station_id) return TrainList.parse_obj(self._get(url)) def _make_url(https://github.com/self,%20path,%20**kwargs): diff --git a/ch15/train-project/train_schedule/cli.py b/ch15/train-project/train_schedule/cli.py index 50a1677..7d11e3a 100644 --- a/ch15/train-project/train_schedule/cli.py +++ b/ch15/train-project/train_schedule/cli.py @@ -56,9 +56,7 @@ def get_arg_parser(): subcommands = parser.add_subparsers( title="Commands", dest="command", - description=( - "Use '%(prog)s COMMAND -h' to get help on a command" - ), + description=("Use '%(prog)s COMMAND -h' to get help on a command"), ) config_parser = subcommands.add_parser( diff --git a/ch15/train-project/train_schedule/gui.py b/ch15/train-project/train_schedule/gui.py index 173b045..3bb9384 100644 --- a/ch15/train-project/train_schedule/gui.py +++ b/ch15/train-project/train_schedule/gui.py @@ -28,9 +28,7 @@ def __init__(self): self.api_client = TrainAPIClient(self.config) # Create models - self.stations_model = StationsModel( - datasource=self.api_client - ) + self.stations_model = StationsModel(datasource=self.api_client) self.arrivals_model = ArrivalsModel( datasource=self.api_client, ) @@ -41,9 +39,7 @@ def __init__(self): # Bind callbacks to handle model events self.stations_model.updated.bind(self.stations_updated) self.arrivals_model.updated.bind(self.arrivals_updated) - self.departures_model.updated.bind( - self.departures_updated - ) + self.departures_model.updated.bind(self.departures_updated) # Create the main window self.mainwindow = MainWindow() @@ -58,13 +54,9 @@ def __init__(self): # Register callbacks to handle UI events self.mainwindow.bind("", self.main_visible) self.mainwindow.bind("<>", self.about) - self.mainwindow.bind( - "<>", self.configure - ) + self.mainwindow.bind("<>", self.configure) self.mainwindow.bind("<>", self.refresh) - self.station_chooser.bind( - "<>", self.station_selected - ) + self.station_chooser.bind("<>", self.station_selected) def run(self): self.mainwindow.run() @@ -105,9 +97,7 @@ def about(self, event=None): def configure(self, event=None): """Show the configuration dialog""" config_dialog = ConfigDialog(self.root, self.config) - config_dialog.bind( - "<>", self.config_updated - ) + config_dialog.bind("<>", self.config_updated) config_dialog.run() def config_updated(self, event=None): diff --git a/ch15/train-project/train_schedule/models/stations.py b/ch15/train-project/train_schedule/models/stations.py index 3a0ac32..17d80a0 100644 --- a/ch15/train-project/train_schedule/models/stations.py +++ b/ch15/train-project/train_schedule/models/stations.py @@ -17,8 +17,7 @@ def update(self): Request updated station data from the data source and emit an `updated` event when the data is updated""" self._stations = { - station.id: station - for station in self._datasource.get_stations() + station.id: station for station in self._datasource.get_stations() } self.updated.emit(stations=self._stations.values()) diff --git a/ch15/train-project/train_schedule/models/trains.py b/ch15/train-project/train_schedule/models/trains.py index dd091c9..ccc2f41 100644 --- a/ch15/train-project/train_schedule/models/trains.py +++ b/ch15/train-project/train_schedule/models/trains.py @@ -24,9 +24,7 @@ def update(self): otherwise clear the train data. Emit an `updated` event when the data is updated""" if self._station is not None: - self._trains = { - train.id: train for train in self._fetch_trains() - } + self._trains = {train.id: train for train in self._fetch_trains()} else: self._trains = {} diff --git a/ch15/train-project/train_schedule/views/config.py b/ch15/train-project/train_schedule/views/config.py index 514613b..8f78dde 100644 --- a/ch15/train-project/train_schedule/views/config.py +++ b/ch15/train-project/train_schedule/views/config.py @@ -50,22 +50,16 @@ def create_buttons(self, buttonbox): ) self.dialog.bind("", lambda e: ok_button.invoke()) - cancel_button = ttk.Button( - buttonbox, text="Cancel", command=self.dismiss - ) + cancel_button = ttk.Button(buttonbox, text="Cancel", command=self.dismiss) - cancel_button.grid( - row=0, column=0, sticky=(tk.N, tk.S, tk.E) - ) + cancel_button.grid(row=0, column=0, sticky=(tk.N, tk.S, tk.E)) ok_button.grid(row=0, column=1, sticky=tk.NSEW) buttonbox.columnconfigure(0, weight=1) def _make_config_vars(self): """Create Tk variables for all the configuration values""" self._config_vars = { - "api_url": tk.StringVar( - self.dialog, self.config.api_url, "api_url" - ), + "api_url": tk.StringVar(self.dialog, self.config.api_url, "api_url"), } def _update_config(self): diff --git a/ch15/train-project/train_schedule/views/dialog.py b/ch15/train-project/train_schedule/views/dialog.py index 10a960a..6e7d47d 100644 --- a/ch15/train-project/train_schedule/views/dialog.py +++ b/ch15/train-project/train_schedule/views/dialog.py @@ -19,9 +19,7 @@ class Dialog: """ def __init__(self, parent, title, resizable=False): - self.dialog = tk.Toplevel( - parent, class_=self.__class__.__name__ - ) + self.dialog = tk.Toplevel(parent, class_=self.__class__.__name__) self._title = title self._resizable = resizable @@ -81,9 +79,7 @@ def run(self): self.dialog.deiconify() self.initial_focus.focus_set() self.dialog.wait_visibility() - self.dialog.resizable( - width=self._resizable, height=self._resizable - ) + self.dialog.resizable(width=self._resizable, height=self._resizable) self.dialog.grab_set() self.dialog.wait_window() diff --git a/ch15/train-project/train_schedule/views/main.py b/ch15/train-project/train_schedule/views/main.py index 1c2c9d6..3f3ed34 100644 --- a/ch15/train-project/train_schedule/views/main.py +++ b/ch15/train-project/train_schedule/views/main.py @@ -43,9 +43,7 @@ def _set_title(self): def _set_icon(self): """Set the window icon""" - self.icon = tk.PhotoImage( - data=load_binary_resource(ICON_FILENAME) - ) + self.icon = tk.PhotoImage(data=load_binary_resource(ICON_FILENAME)) self.root.iconphoto(True, self.icon) def _make_menus(self): @@ -64,9 +62,7 @@ def _make_app_menu(self): app_menu = tk.Menu(self.menubar) app_menu.add_command( label="Refresh", - command=lambda: self.root.event_generate( - "<>" - ), + command=lambda: self.root.event_generate("<>"), underline=0, ) app_menu.add_command( @@ -74,37 +70,27 @@ def _make_app_menu(self): command=self.quit, underline=0, ) - self.menubar.add_cascade( - menu=app_menu, label=self.title, underline=0 - ) + self.menubar.add_cascade(menu=app_menu, label=self.title, underline=0) def _make_edit_menu(self): """Create the 'Edit' menu""" edit_menu = tk.Menu(self.menubar) edit_menu.add_command( label="Preferences...", - command=lambda: self.root.event_generate( - "<>" - ), + command=lambda: self.root.event_generate("<>"), underline=0, ) - self.menubar.add_cascade( - menu=edit_menu, label="Edit", underline=0 - ) + self.menubar.add_cascade(menu=edit_menu, label="Edit", underline=0) def _make_help_menu(self): """Create the 'Help' menu""" help_menu = tk.Menu(self.menubar) help_menu.add_command( label="About...", - command=lambda: self.root.event_generate( - "<>" - ), + command=lambda: self.root.event_generate("<>"), underline=0, ) - self.menubar.add_cascade( - menu=help_menu, label="Help", underline=0 - ) + self.menubar.add_cascade(menu=help_menu, label="Help", underline=0) def _make_content(self): """Create the widgets to populate the body of the @@ -113,9 +99,7 @@ def _make_content(self): station_frame = self._make_station_chooser(content_frame) station_frame.grid(row=0, column=0, sticky=tk.NSEW) - notebook = ttk.Notebook( - content_frame, padding=(0, 5, 0, 0) - ) + notebook = ttk.Notebook(content_frame, padding=(0, 5, 0, 0)) self.arrivals_view = self._make_train_tab( notebook, "Arrivals", show_from=True, show_to=False ) @@ -136,9 +120,7 @@ def _make_station_chooser(self, content_frame): content_frame, text="Station", padding=(5, 5, 5, 5) ) self.station_chooser = StationChooser(station_frame) - self.station_chooser.combobox.grid( - row=0, column=0, sticky=tk.NSEW - ) + self.station_chooser.combobox.grid(row=0, column=0, sticky=tk.NSEW) station_frame.columnconfigure(0, weight=1) return station_frame @@ -149,17 +131,13 @@ def _make_train_tab(self, notebook, name, show_from, show_to): frame = ttk.Frame(notebook, padding=(5, 5, 5, 5)) notebook.add(frame, text=name) - train_view = TrainsView( - frame, show_from=show_from, show_to=show_to - ) + train_view = TrainsView(frame, show_from=show_from, show_to=show_to) scrollbar = ttk.Scrollbar( frame, orient=tk.VERTICAL, command=train_view.treeview.yview, ) - train_view.treeview.configure( - yscrollcommand=scrollbar.set - ) + train_view.treeview.configure(yscrollcommand=scrollbar.set) train_view.treeview.grid(row=0, column=0, sticky=tk.NSEW) scrollbar.grid(row=0, column=1, sticky=tk.NS) diff --git a/requirements.in b/requirements.in new file mode 100644 index 0000000..3fb7501 --- /dev/null +++ b/requirements.in @@ -0,0 +1,27 @@ +arrow +requests +sqlalchemy +pyjwt +cryptography +marshmallow +pytest +pdbpp +bs4 +requests +faker +jupyter +jupyterlab +matplotlib +numpy +openpyxl +pandas +build +twine +platformdirs +pydantic +isort +black +django +fastapi +python-dotenv +uvicorn \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..cc3e9a5 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,499 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile ../requirements.in +# +annotated-types==0.5.0 + # via pydantic +anyio==4.0.0 + # via + # httpx + # jupyter-server + # starlette +argon2-cffi==23.1.0 + # via jupyter-server +argon2-cffi-bindings==21.2.0 + # via argon2-cffi +arrow==1.3.0 + # via + # -r requirements.in + # isoduration +asgiref==3.8.1 + # via django +asttokens==2.4.0 + # via stack-data +async-lru==2.0.4 + # via jupyterlab +attrs==23.1.0 + # via + # jsonschema + # referencing + # wmctrl +babel==2.12.1 + # via jupyterlab-server +backcall==0.2.0 + # via ipython +beautifulsoup4==4.12.2 + # via + # bs4 + # nbconvert +black==24.8.0 + # via -r requirements.in +bleach==6.0.0 + # via nbconvert +bs4==0.0.2 + # via -r requirements.in +build==1.2.1 + # via -r requirements.in +certifi==2024.7.4 + # via + # httpcore + # httpx + # requests +cffi==1.16.0 + # via + # argon2-cffi-bindings + # cryptography +charset-normalizer==3.3.0 + # via requests +click==8.1.7 + # via + # black + # uvicorn +comm==0.1.4 + # via + # ipykernel + # ipywidgets +contourpy==1.1.1 + # via matplotlib +cryptography==43.0.1 + # via + # -r requirements.in + # secretstorage +cycler==0.12.0 + # via matplotlib +debugpy==1.8.0 + # via ipykernel +decorator==5.1.1 + # via ipython +defusedxml==0.7.1 + # via nbconvert +django==5.1 + # via -r requirements.in +docutils==0.20.1 + # via readme-renderer +et-xmlfile==1.1.0 + # via openpyxl +executing==2.0.0 + # via stack-data +faker==27.0.0 + # via -r requirements.in +fancycompleter==0.9.1 + # via pdbpp +fastapi==0.115.2 + # via -r requirements.in +fastjsonschema==2.18.1 + # via nbformat +fonttools==4.43.0 + # via matplotlib +fqdn==1.5.1 + # via jsonschema +greenlet==2.0.2 + # via sqlalchemy +h11==0.14.0 + # via + # httpcore + # uvicorn +httpcore==1.0.5 + # via httpx +httpx==0.27.0 + # via jupyterlab +idna==3.7 + # via + # anyio + # httpx + # jsonschema + # requests +importlib-metadata==6.8.0 + # via + # keyring + # twine +iniconfig==2.0.0 + # via pytest +ipykernel==6.25.2 + # via + # jupyter + # jupyter-console + # jupyterlab + # qtconsole +ipython==8.16.0 + # via + # ipykernel + # ipywidgets + # jupyter-console +ipython-genutils==0.2.0 + # via qtconsole +ipywidgets==8.1.1 + # via jupyter +isoduration==20.11.0 + # via jsonschema +isort==5.13.2 + # via -r requirements.in +jaraco-classes==3.3.0 + # via keyring +jedi==0.19.0 + # via ipython +jeepney==0.8.0 + # via + # keyring + # secretstorage +jinja2==3.1.3 + # via + # jupyter-server + # jupyterlab + # jupyterlab-server + # nbconvert +json5==0.9.14 + # via jupyterlab-server +jsonpointer==2.4 + # via jsonschema +jsonschema[format-nongpl]==4.19.1 + # via + # jupyter-events + # jupyterlab-server + # nbformat +jsonschema-specifications==2023.7.1 + # via jsonschema +jupyter==1.0.0 + # via -r requirements.in +jupyter-client==8.3.1 + # via + # ipykernel + # jupyter-console + # jupyter-server + # nbclient + # qtconsole +jupyter-console==6.6.3 + # via jupyter +jupyter-core==5.3.2 + # via + # ipykernel + # jupyter-client + # jupyter-console + # jupyter-server + # jupyterlab + # nbclient + # nbconvert + # nbformat + # qtconsole +jupyter-events==0.9.0 + # via jupyter-server +jupyter-lsp==2.2.2 + # via jupyterlab +jupyter-server==2.11.2 + # via + # jupyter-lsp + # jupyterlab + # jupyterlab-server + # notebook + # notebook-shim +jupyter-server-terminals==0.4.4 + # via jupyter-server +jupyterlab==4.2.5 + # via + # -r requirements.in + # notebook +jupyterlab-pygments==0.2.2 + # via nbconvert +jupyterlab-server==2.27.3 + # via + # jupyterlab + # notebook +jupyterlab-widgets==3.0.9 + # via ipywidgets +keyring==24.2.0 + # via twine +kiwisolver==1.4.5 + # via matplotlib +markdown-it-py==3.0.0 + # via rich +markupsafe==2.1.3 + # via + # jinja2 + # nbconvert +marshmallow==3.22.0 + # via -r requirements.in +matplotlib==3.8.4 + # via -r requirements.in +matplotlib-inline==0.1.6 + # via + # ipykernel + # ipython +mdurl==0.1.2 + # via markdown-it-py +mistune==3.0.2 + # via nbconvert +more-itertools==10.1.0 + # via jaraco-classes +mypy-extensions==1.0.0 + # via black +nbclient==0.8.0 + # via nbconvert +nbconvert==7.8.0 + # via + # jupyter + # jupyter-server +nbformat==5.9.2 + # via + # jupyter-server + # nbclient + # nbconvert +nest-asyncio==1.5.8 + # via ipykernel +nh3==0.2.14 + # via readme-renderer +notebook==7.2.2 + # via jupyter +notebook-shim==0.2.3 + # via + # jupyterlab + # notebook +numpy==1.26.0 + # via + # -r requirements.in + # contourpy + # matplotlib + # pandas +openpyxl==3.1.5 + # via -r requirements.in +overrides==7.4.0 + # via jupyter-server +packaging==23.2 + # via + # black + # build + # ipykernel + # jupyter-server + # jupyterlab + # jupyterlab-server + # marshmallow + # matplotlib + # nbconvert + # pytest + # qtconsole + # qtpy +pandas==2.2.2 + # via -r requirements.in +pandocfilters==1.5.0 + # via nbconvert +parso==0.8.3 + # via jedi +pathspec==0.11.2 + # via black +pdbpp==0.10.3 + # via -r requirements.in +pexpect==4.8.0 + # via ipython +pickleshare==0.7.5 + # via ipython +pillow==10.3.0 + # via matplotlib +pkginfo==1.9.6 + # via twine +platformdirs==4.2.2 + # via + # -r requirements.in + # black + # jupyter-core +pluggy==1.5.0 + # via pytest +prometheus-client==0.17.1 + # via jupyter-server +prompt-toolkit==3.0.39 + # via + # ipython + # jupyter-console +psutil==5.9.5 + # via ipykernel +ptyprocess==0.7.0 + # via + # pexpect + # terminado +pure-eval==0.2.2 + # via stack-data +pycparser==2.21 + # via cffi +pydantic==2.8.2 + # via + # -r requirements.in + # fastapi +pydantic-core==2.20.1 + # via pydantic +pygments==2.16.1 + # via + # ipython + # jupyter-console + # nbconvert + # pdbpp + # qtconsole + # readme-renderer + # rich +pyjwt==2.8.0 + # via -r requirements.in +pyparsing==3.1.1 + # via matplotlib +pyproject-hooks==1.0.0 + # via build +pyrepl==0.9.0 + # via fancycompleter +pytest==8.2.2 + # via -r requirements.in +python-dateutil==2.8.2 + # via + # arrow + # faker + # jupyter-client + # matplotlib + # pandas +python-dotenv==1.0.1 + # via -r requirements.in +python-json-logger==2.0.7 + # via jupyter-events +pytz==2023.3.post1 + # via pandas +pyyaml==6.0.1 + # via jupyter-events +pyzmq==25.1.1 + # via + # ipykernel + # jupyter-client + # jupyter-console + # jupyter-server + # qtconsole +qtconsole==5.4.4 + # via jupyter +qtpy==2.4.0 + # via qtconsole +readme-renderer==42.0 + # via twine +referencing==0.30.2 + # via + # jsonschema + # jsonschema-specifications + # jupyter-events +requests==2.32.3 + # via + # -r requirements.in + # jupyterlab-server + # requests-toolbelt + # twine +requests-toolbelt==1.0.0 + # via twine +rfc3339-validator==0.1.4 + # via + # jsonschema + # jupyter-events +rfc3986==2.0.0 + # via twine +rfc3986-validator==0.1.1 + # via + # jsonschema + # jupyter-events +rich==13.6.0 + # via twine +rpds-py==0.10.3 + # via + # jsonschema + # referencing +secretstorage==3.3.3 + # via keyring +send2trash==1.8.2 + # via jupyter-server +six==1.16.0 + # via + # asttokens + # bleach + # python-dateutil + # rfc3339-validator +sniffio==1.3.0 + # via + # anyio + # httpx +soupsieve==2.5 + # via beautifulsoup4 +sqlalchemy==2.0.32 + # via -r requirements.in +sqlparse==0.5.0 + # via django +stack-data==0.6.3 + # via ipython +starlette==0.37.2 + # via fastapi +terminado==0.17.1 + # via + # jupyter-server + # jupyter-server-terminals +tinycss2==1.2.1 + # via nbconvert +tornado==6.4.1 + # via + # ipykernel + # jupyter-client + # jupyter-server + # jupyterlab + # notebook + # terminado +traitlets==5.10.1 + # via + # comm + # ipykernel + # ipython + # ipywidgets + # jupyter-client + # jupyter-console + # jupyter-core + # jupyter-events + # jupyter-server + # jupyterlab + # matplotlib-inline + # nbclient + # nbconvert + # nbformat + # qtconsole +twine==5.1.1 + # via -r requirements.in +types-python-dateutil==2.8.19.14 + # via arrow +typing-extensions==4.8.0 + # via + # fastapi + # pydantic + # pydantic-core + # sqlalchemy +tzdata==2023.3 + # via pandas +uri-template==1.3.0 + # via jsonschema +urllib3==2.2.2 + # via + # requests + # twine +uvicorn==0.23.2 + # via -r requirements.in +wcwidth==0.2.8 + # via prompt-toolkit +webcolors==1.13 + # via jsonschema +webencodings==0.5.1 + # via + # bleach + # tinycss2 +websocket-client==1.6.3 + # via jupyter-server +widgetsnbextension==4.0.9 + # via ipywidgets +wmctrl==0.5 + # via pdbpp +zipp==3.19.1 + # via importlib-metadata