diff --git a/Student/rdrovdahl/README.rst b/Student/rdrovdahl/README.rst new file mode 100644 index 0000000..30e55a6 --- /dev/null +++ b/Student/rdrovdahl/README.rst @@ -0,0 +1 @@ +Python code for UWPCE-PythonCert class, written by Ryan Drovdahl diff --git a/Student/rdrovdahl/lesson01/README.rst b/Student/rdrovdahl/lesson01/README.rst new file mode 100644 index 0000000..30e55a6 --- /dev/null +++ b/Student/rdrovdahl/lesson01/README.rst @@ -0,0 +1 @@ +Python code for UWPCE-PythonCert class, written by Ryan Drovdahl diff --git a/Student/rdrovdahl/lesson01/featuresdf.csv b/Student/rdrovdahl/lesson01/featuresdf.csv new file mode 100644 index 0000000..ece51ea --- /dev/null +++ b/Student/rdrovdahl/lesson01/featuresdf.csv @@ -0,0 +1,101 @@ +id,name,artists,danceability,energy,key,loudness,mode,speechiness,acousticness,instrumentalness,liveness,valence,tempo,duration_ms,time_signature +7qiZfU4dY1lWllzX7mPBI,Shape of You,Ed Sheeran,0.825,0.652,1.0,-3.183,0.0,0.0802,0.581,0.0,0.0931,0.931,95.977,233713.0,4.0 +5CtI0qwDJkDQGwXD1H1cL,Despacito - Remix,Luis Fonsi,0.694,0.815,2.0,-4.328,1.0,0.12,0.229,0.0,0.0924,0.813,88.931,228827.0,4.0 +4aWmUDTfIPGksMNLV2rQP,Despacito (Featuring Daddy Yankee),Luis Fonsi,0.66,0.786,2.0,-4.757,1.0,0.17,0.209,0.0,0.112,0.846,177.833,228200.0,4.0 +6RUKPb4LETWmmr3iAEQkt,Something Just Like This,The Chainsmokers,0.617,0.635,11.0,-6.769,0.0,0.0317,0.0498,1.44e-05,0.164,0.446,103.019,247160.0,4.0 +3DXncPQOG4VBw3QHh3S81,I'm the One,DJ Khaled,0.609,0.668,7.0,-4.284,1.0,0.0367,0.0552,0.0,0.167,0.811,80.924,288600.0,4.0 +7KXjTSCq5nL1LoYtL7XAw,HUMBLE.,Kendrick Lamar,0.904,0.611,1.0,-6.842,0.0,0.0888,0.000259,2.03e-05,0.0976,0.4,150.02,177000.0,4.0 +3eR23VReFzcdmS7TYCrhC,It Ain't Me (with Selena Gomez),Kygo,0.64,0.533,0.0,-6.596,1.0,0.0706,0.119,0.0,0.0864,0.515,99.968,220781.0,4.0 +3B54sVLJ402zGa6Xm4YGN,Unforgettable,French Montana,0.726,0.769,6.0,-5.043,1.0,0.123,0.0293,0.0101,0.104,0.733,97.985,233902.0,4.0 +0KKkJNfGyhkQ5aFogxQAP,That's What I Like,Bruno Mars,0.853,0.56,1.0,-4.961,1.0,0.0406,0.013,0.0,0.0944,0.86,134.066,206693.0,4.0 +3NdDpSvN911VPGivFlV5d,"I Don’t Wanna Live Forever (Fifty Shades Darker) - From ""Fifty Shades Darker (Original Motion Picture Soundtrack)""",ZAYN,0.735,0.451,0.0,-8.374,1.0,0.0585,0.0631,1.3e-05,0.325,0.0862,117.973,245200.0,4.0 +7GX5flRQZVHRAGd6B4TmD,XO TOUR Llif3,Lil Uzi Vert,0.732,0.75,11.0,-6.366,0.0,0.231,0.00264,0.0,0.109,0.401,155.096,182707.0,4.0 +72jbDTw1piOOj770jWNea,Paris,The Chainsmokers,0.653,0.658,2.0,-6.428,1.0,0.0304,0.0215,1.66e-06,0.0939,0.219,99.99,221507.0,4.0 +0dA2Mk56wEzDgegdC6R17,Stay (with Alessia Cara),Zedd,0.679,0.634,5.0,-5.024,0.0,0.0654,0.232,0.0,0.115,0.498,102.013,210091.0,4.0 +4iLqG9SeJSnt0cSPICSjx,Attention,Charlie Puth,0.774,0.626,3.0,-4.432,0.0,0.0432,0.0969,3.12e-05,0.0848,0.777,100.041,211475.0,4.0 +0VgkVdmE4gld66l8iyGjg,Mask Off,Future,0.833,0.434,2.0,-8.795,1.0,0.431,0.0102,0.0219,0.165,0.281,150.062,204600.0,4.0 +3a1lNhkSLSkpJE4MSHpDu,Congratulations,Post Malone,0.627,0.812,6.0,-4.215,1.0,0.0358,0.198,0.0,0.212,0.504,123.071,220293.0,4.0 +6kex4EBAj0WHXDKZMEJaa,Swalla (feat. Nicki Minaj & Ty Dolla $ign),Jason Derulo,0.696,0.817,1.0,-3.862,1.0,0.109,0.075,0.0,0.187,0.782,98.064,216409.0,4.0 +6PCUP3dWmTjcTtXY02oFd,Castle on the Hill,Ed Sheeran,0.461,0.834,2.0,-4.868,1.0,0.0989,0.0232,1.14e-05,0.14,0.471,135.007,261154.0,4.0 +5knuzwU65gJK7IF5yJsua,Rockabye (feat. Sean Paul & Anne-Marie),Clean Bandit,0.72,0.763,9.0,-4.068,0.0,0.0523,0.406,0.0,0.18,0.742,101.965,251088.0,4.0 +0CcQNd8CINkwQfe1RDtGV,Believer,Imagine Dragons,0.779,0.787,10.0,-4.305,0.0,0.108,0.0524,0.0,0.14,0.708,124.982,204347.0,4.0 +2rb5MvYT7ZIxbKW5hfcHx,Mi Gente,J Balvin,0.543,0.677,11.0,-4.915,0.0,0.0993,0.0148,6.21e-06,0.13,0.294,103.809,189440.0,4.0 +0tKcYR2II1VCQWT79i5Nr,Thunder,Imagine Dragons,0.6,0.81,0.0,-4.749,1.0,0.0479,0.00683,0.21,0.155,0.298,167.88,187147.0,4.0 +5uCax9HTNlzGybIStD3vD,Say You Won't Let Go,James Arthur,0.358,0.557,10.0,-7.398,1.0,0.059,0.695,0.0,0.0902,0.494,85.043,211467.0,4.0 +79cuOz3SPQTuFrp8WgftA,There's Nothing Holdin' Me Back,Shawn Mendes,0.857,0.8,2.0,-4.035,1.0,0.0583,0.381,0.0,0.0913,0.966,121.996,199440.0,4.0 +6De0lHrwBfPfrhorm9q1X,Me Rehúso,Danny Ocean,0.744,0.804,1.0,-6.327,1.0,0.0677,0.0231,0.0,0.0494,0.426,104.823,205715.0,4.0 +6D0b04NJIKfEMg040WioJ,Issues,Julia Michaels,0.706,0.427,8.0,-6.864,1.0,0.0879,0.413,0.0,0.0609,0.42,113.804,176320.0,4.0 +0afhq8XCExXpqazXczTSv,Galway Girl,Ed Sheeran,0.624,0.876,9.0,-3.374,1.0,0.1,0.0735,0.0,0.327,0.781,99.943,170827.0,4.0 +3ebXMykcMXOcLeJ9xZ17X,Scared to Be Lonely,Martin Garrix,0.584,0.54,1.0,-7.786,0.0,0.0576,0.0895,0.0,0.261,0.195,137.972,220883.0,4.0 +7BKLCZ1jbUBVqRi2FVlTV,Closer,The Chainsmokers,0.748,0.524,8.0,-5.599,1.0,0.0338,0.414,0.0,0.111,0.661,95.01,244960.0,4.0 +1x5sYLZiu9r5E43kMlt9f,Symphony (feat. Zara Larsson),Clean Bandit,0.707,0.629,0.0,-4.581,0.0,0.0563,0.259,1.6e-05,0.138,0.457,122.863,212459.0,4.0 +5GXAXm5YOmYT0kL5jHvYB,I Feel It Coming,The Weeknd,0.768,0.813,0.0,-5.94,0.0,0.128,0.427,0.0,0.102,0.579,92.994,269187.0,4.0 +5aAx2yezTd8zXrkmtKl66,Starboy,The Weeknd,0.681,0.594,7.0,-7.028,1.0,0.282,0.165,3.49e-06,0.134,0.535,186.054,230453.0,4.0 +1OAh8uOEOvTDqkKFsKksC,Wild Thoughts,DJ Khaled,0.671,0.672,0.0,-3.094,0.0,0.0688,0.0329,0.0,0.118,0.632,97.98,204173.0,4.0 +7tr2za8SQg2CI8EDgrdtN,Slide,Calvin Harris,0.736,0.795,1.0,-3.299,0.0,0.0545,0.498,1.21e-06,0.254,0.511,104.066,230813.0,4.0 +2ekn2ttSfGqwhhate0LSR,New Rules,Dua Lipa,0.771,0.696,9.0,-6.258,0.0,0.0755,0.00256,9.71e-06,0.179,0.656,116.054,208827.0,4.0 +5tz69p7tJuGPeMGwNTxYu,1-800-273-8255,Logic,0.629,0.572,5.0,-7.733,0.0,0.0387,0.57,0.0,0.192,0.386,100.015,250173.0,4.0 +7hDc8b7IXETo14hHIHdnh,Passionfruit,Drake,0.809,0.463,11.0,-11.377,1.0,0.0396,0.256,0.085,0.109,0.364,111.98,298941.0,4.0 +7wGoVu4Dady5GV0Sv4UIs,rockstar,Post Malone,0.577,0.522,5.0,-6.594,0.0,0.0984,0.13,9.03e-05,0.142,0.119,159.772,218320.0,4.0 +6EpRaXYhGOB3fj4V2uDkM,Strip That Down,Liam Payne,0.869,0.485,6.0,-5.595,1.0,0.0545,0.246,0.0,0.0765,0.527,106.028,204502.0,4.0 +3A7qX2QjDlPnazUsRk5y0,2U (feat. Justin Bieber),David Guetta,0.548,0.65,8.0,-5.827,0.0,0.0591,0.219,0.0,0.225,0.557,144.937,194897.0,4.0 +0tgVpDi06FyKpA1z0VMD4,Perfect,Ed Sheeran,0.599,0.448,8.0,-6.312,1.0,0.0232,0.163,0.0,0.106,0.168,95.05,263400.0,3.0 +78rIJddV4X0HkNAInEcYd,Call On Me - Ryan Riback Extended Remix,Starley,0.676,0.843,0.0,-4.068,1.0,0.0367,0.0623,0.000752,0.181,0.718,105.003,222041.0,4.0 +5bcTCxgc7xVfSaMV3RuVk,Feels,Calvin Harris,0.893,0.745,11.0,-3.105,0.0,0.0571,0.0642,0.0,0.0943,0.872,101.018,223413.0,4.0 +0NiXXAI876aGImAd6rTj8,Mama,Jonas Blue,0.746,0.793,11.0,-4.209,0.0,0.0412,0.11,0.0,0.0528,0.557,104.027,181615.0,4.0 +0qYTZCo5Bwh1nsUFGZP3z,Felices los 4,Maluma,0.755,0.789,5.0,-4.502,1.0,0.146,0.231,0.0,0.351,0.737,93.973,229849.0,4.0 +2EEeOnHehOozLq4aS0n6S,iSpy (feat. Lil Yachty),KYLE,0.746,0.653,7.0,-6.745,1.0,0.289,0.378,0.0,0.229,0.672,75.016,253107.0,4.0 +152lZdxL1OR0ZMW6KquMi,Location,Khalid,0.736,0.449,1.0,-11.462,0.0,0.425,0.33,0.000162,0.0898,0.326,80.126,219080.0,4.0 +6mICuAdrwEjh6Y6lroV2K,Chantaje,Shakira,0.852,0.773,8.0,-2.921,0.0,0.0776,0.187,3.05e-05,0.159,0.907,102.034,195840.0,4.0 +4Km5HrUvYTaSUfiSGPJeQ,Bad and Boujee (feat. Lil Uzi Vert),Migos,0.927,0.665,11.0,-5.313,1.0,0.244,0.061,0.0,0.123,0.175,127.076,343150.0,4.0 +0ofbQMrRDsUaVKq2mGLEA,Havana,Camila Cabello,0.768,0.517,7.0,-4.323,0.0,0.0312,0.186,3.8e-05,0.104,0.418,104.992,216897.0,4.0 +6HUnnBwYZqcED1eQztxMB,Solo Dance,Martin Jensen,0.744,0.836,6.0,-2.396,0.0,0.0507,0.0435,0.0,0.194,0.36,114.965,174933.0,4.0 +343YBumqHu19cGoGARUTs,Fake Love,Drake,0.927,0.488,9.0,-9.433,0.0,0.42,0.108,0.0,0.196,0.605,133.987,210937.0,4.0 +4pdPtRcBmOSQDlJ3Fk945,Let Me Love You,DJ Snake,0.476,0.718,8.0,-5.309,1.0,0.0576,0.0784,1.02e-05,0.122,0.142,199.864,205947.0,4.0 +3PEgB3fkiojxms35ntsTg,More Than You Know,Axwell /\ Ingrosso,0.644,0.743,5.0,-5.002,0.0,0.0355,0.034,0.0,0.257,0.544,123.074,203000.0,4.0 +1xznGGDReH1oQq0xzbwXa,One Dance,Drake,0.791,0.619,1.0,-5.886,1.0,0.0532,0.00784,0.00423,0.351,0.371,103.989,173987.0,4.0 +7nKBxz47S9SD79N086fuh,SUBEME LA RADIO,Enrique Iglesias,0.684,0.823,9.0,-3.297,0.0,0.0773,0.0744,0.0,0.111,0.647,91.048,208163.0,4.0 +1NDxZ7cFAo481dtYWdrUn,Pretty Girl - Cheat Codes X CADE Remix,Maggie Lindemann,0.703,0.868,7.0,-4.661,0.0,0.0291,0.15,0.132,0.104,0.733,121.03,193613.0,4.0 +3m660poUr1chesgkkjQM7,Sorry Not Sorry,Demi Lovato,0.704,0.633,11.0,-6.923,0.0,0.241,0.0214,0.0,0.29,0.863,144.021,203760.0,4.0 +3kxfsdsCpFgN412fpnW85,Redbone,Childish Gambino,0.743,0.359,1.0,-10.401,1.0,0.0794,0.199,0.00611,0.137,0.587,160.083,326933.0,4.0 +6b8Be6ljOzmkOmFslEb23,24K Magic,Bruno Mars,0.818,0.803,1.0,-4.282,1.0,0.0797,0.034,0.0,0.153,0.632,106.97,225983.0,4.0 +6HZILIRieu8S0iqY8kIKh,DNA.,Kendrick Lamar,0.637,0.514,1.0,-6.763,1.0,0.365,0.0047,0.0,0.094,0.402,139.931,185947.0,4.0 +3umS4y3uQDkqekNjVpiRU,El Amante,Nicky Jam,0.683,0.691,8.0,-5.535,1.0,0.0432,0.243,0.0,0.14,0.732,179.91,219507.0,4.0 +00lNx0OcTJrS3MKHcB80H,You Don't Know Me - Radio Edit,Jax Jones,0.876,0.669,11.0,-6.054,0.0,0.138,0.163,0.0,0.185,0.682,124.007,213947.0,4.0 +6520aj0B4FSKGVuKNsOCO,Chained To The Rhythm,Katy Perry,0.448,0.801,0.0,-5.363,1.0,0.165,0.0733,0.0,0.146,0.462,189.798,237734.0,4.0 +1louJpMmzEicAn7lzDalP,No Promises (feat. Demi Lovato),Cheat Codes,0.741,0.667,10.0,-5.445,1.0,0.134,0.0575,0.0,0.106,0.595,112.956,223504.0,4.0 +2QbFClFyhMMtiurUjuQlA,Don't Wanna Know (feat. Kendrick Lamar),Maroon 5,0.775,0.617,7.0,-6.166,1.0,0.0701,0.341,0.0,0.0985,0.485,100.048,214265.0,4.0 +5hYTyyh2odQKphUbMqc5g,"How Far I'll Go - From ""Moana""",Alessia Cara,0.314,0.555,9.0,-9.601,1.0,0.37,0.157,0.000108,0.067,0.159,179.666,175517.0,4.0 +38yBBH2jacvDxrznF7h08,Slow Hands,Niall Horan,0.734,0.418,0.0,-6.678,1.0,0.0425,0.0129,0.0,0.0579,0.868,85.909,188174.0,4.0 +2cnKEkpVUSV4wnjQiTWfH,Escápate Conmigo,Wisin,0.747,0.864,8.0,-3.181,0.0,0.0599,0.0245,4.46e-05,0.0853,0.754,92.028,232787.0,4.0 +0SGkqnVQo9KPytSri1H6c,Bounce Back,Big Sean,0.77,0.567,2.0,-5.698,1.0,0.175,0.105,0.0,0.125,0.26,81.477,222360.0,4.0 +5Ohxk2dO5COHF1krpoPig,Sign of the Times,Harry Styles,0.516,0.595,5.0,-4.63,1.0,0.0313,0.0275,0.0,0.109,0.222,119.972,340707.0,4.0 +6gBFPUFcJLzWGx4lenP6h,goosebumps,Travis Scott,0.841,0.728,7.0,-3.37,1.0,0.0484,0.0847,0.0,0.149,0.43,130.049,243837.0,4.0 +5Z3GHaZ6ec9bsiI5Benrb,Young Dumb & Broke,Khalid,0.798,0.539,1.0,-6.351,1.0,0.0421,0.199,1.66e-05,0.165,0.394,136.949,202547.0,4.0 +6jA8HL9i4QGzsj6fjoxp8,There for You,Martin Garrix,0.611,0.644,6.0,-7.607,0.0,0.0553,0.124,0.0,0.124,0.13,105.969,221904.0,4.0 +21TdkDRXuAB3k90ujRU1e,Cold (feat. Future),Maroon 5,0.697,0.716,9.0,-6.288,0.0,0.113,0.118,0.0,0.0424,0.506,99.905,234308.0,4.0 +7vGuf3Y35N4wmASOKLUVV,Silence,Marshmello,0.52,0.761,4.0,-3.093,1.0,0.0853,0.256,4.96e-06,0.17,0.286,141.971,180823.0,4.0 +1mXVgsBdtIVeCLJnSnmtd,Too Good At Goodbyes,Sam Smith,0.698,0.375,5.0,-8.279,1.0,0.0491,0.652,0.0,0.173,0.534,91.92,201000.0,4.0 +3EmmCZoqpWOTY1g2GBwJo,Just Hold On,Steve Aoki,0.647,0.932,11.0,-3.515,1.0,0.0824,0.00383,1.5e-06,0.0574,0.374,114.991,198774.0,4.0 +6uFsE1JgZ20EXyU0JQZbU,Look What You Made Me Do,Taylor Swift,0.773,0.68,9.0,-6.378,0.0,0.141,0.213,1.57e-05,0.122,0.497,128.062,211859.0,4.0 +0CokSRCu5hZgPxcZBaEzV,Glorious (feat. Skylar Grey),Macklemore,0.731,0.794,0.0,-5.126,0.0,0.0522,0.0323,2.59e-05,0.112,0.356,139.994,220454.0,4.0 +6875MeXyCW0wLyT72Eetm,Starving,Hailee Steinfeld,0.721,0.626,4.0,-4.2,1.0,0.123,0.402,0.0,0.102,0.558,99.914,181933.0,4.0 +3AEZUABDXNtecAOSC1qTf,Reggaetón Lento (Bailemos),CNCO,0.761,0.838,4.0,-3.073,0.0,0.0502,0.4,0.0,0.176,0.71,93.974,222560.0,4.0 +3E2Zh20GDCR9B1EYjfXWy,Weak,AJR,0.673,0.637,5.0,-4.518,1.0,0.0429,0.137,0.0,0.184,0.678,123.98,201160.0,4.0 +4pLwZjInHj3SimIyN9SnO,Side To Side,Ariana Grande,0.648,0.738,6.0,-5.883,0.0,0.247,0.0408,0.0,0.292,0.603,159.145,226160.0,4.0 +3QwBODjSEzelZyVjxPOHd,Otra Vez (feat. J Balvin),Zion & Lennox,0.832,0.772,10.0,-5.429,1.0,0.1,0.0559,0.000486,0.44,0.704,96.016,209453.0,4.0 +1wjzFQodRWrPcQ0AnYnvQ,I Like Me Better,Lauv,0.752,0.505,9.0,-7.621,1.0,0.253,0.535,2.55e-06,0.104,0.419,91.97,197437.0,4.0 +04DwTuZ2VBdJCCC5TROn7,In the Name of Love,Martin Garrix,0.49,0.485,4.0,-6.237,0.0,0.0406,0.0592,0.0,0.337,0.196,133.889,195840.0,4.0 +6DNtNfH8hXkqOX1sjqmI7,Cold Water (feat. Justin Bieber & MØ),Major Lazer,0.608,0.798,6.0,-5.092,0.0,0.0432,0.0736,0.0,0.156,0.501,92.943,185352.0,4.0 +1UZOjK1BwmwWU14Erba9C,Malibu,Miley Cyrus,0.573,0.781,8.0,-6.406,1.0,0.0555,0.0767,2.64e-05,0.0813,0.343,139.934,231907.0,4.0 +4b4KcovePX8Ke2cLIQTLM,All Night,The Vamps,0.544,0.809,8.0,-5.098,1.0,0.0363,0.0038,0.0,0.323,0.448,145.017,197640.0,4.0 +1a5Yu5L18qNxVhXx38njO,Hear Me Now,Alok,0.789,0.442,11.0,-7.844,1.0,0.0421,0.586,0.00366,0.0927,0.45,121.971,192846.0,4.0 +4c2W3VKsOFoIg2SFaO6DY,Your Song,Rita Ora,0.855,0.624,1.0,-4.093,1.0,0.0488,0.158,0.0,0.0513,0.962,117.959,180757.0,4.0 +22eADXu8DfOAUEDw4vU8q,Ahora Dice,Chris Jeday,0.708,0.693,6.0,-5.516,1.0,0.138,0.246,0.0,0.129,0.427,143.965,271080.0,4.0 +7nZmah2llfvLDiUjm0kiy,Friends (with BloodPop®),Justin Bieber,0.744,0.739,8.0,-5.35,1.0,0.0387,0.00459,0.0,0.306,0.649,104.99,189467.0,4.0 +2fQrGHiQOvpL9UgPvtYy6,Bank Account,21 Savage,0.884,0.346,8.0,-8.228,0.0,0.351,0.0151,7.04e-06,0.0871,0.376,75.016,220307.0,4.0 +1PSBzsahR2AKwLJgx8ehB,Bad Things (with Camila Cabello),Machine Gun Kelly,0.675,0.69,2.0,-4.761,1.0,0.132,0.21,0.0,0.287,0.272,137.817,239293.0,4.0 +0QsvXIfqM0zZoerQfsI9l,Don't Let Me Down,The Chainsmokers,0.542,0.859,11.0,-5.651,1.0,0.197,0.16,0.00466,0.137,0.403,159.797,208053.0,4.0 +7mldq42yDuxiUNn08nvzH,Body Like A Back Road,Sam Hunt,0.731,0.469,5.0,-7.226,1.0,0.0326,0.463,1.04e-06,0.103,0.631,98.963,165387.0,4.0 +7i2DJ88J7jQ8K7zqFX2fW,Now Or Never,Halsey,0.658,0.588,6.0,-4.902,0.0,0.0367,0.105,1.28e-06,0.125,0.434,110.075,214802.0,4.0 +1j4kHkkpqZRBwE0A4CN4Y,Dusk Till Dawn - Radio Edit,ZAYN,0.258,0.437,11.0,-6.593,0.0,0.039,0.101,1.27e-06,0.106,0.0967,180.043,239000.0,4.0 diff --git a/Student/rdrovdahl/lesson01/generator_solution.py b/Student/rdrovdahl/lesson01/generator_solution.py new file mode 100644 index 0000000..21b3eec --- /dev/null +++ b/Student/rdrovdahl/lesson01/generator_solution.py @@ -0,0 +1,55 @@ +#! /usr/local/bin/python3 + + +''' +A few simple generators +''' + + +def intsum(): + 'generator to keep adding the next integer' + i = 0 + current_value = 0 + while True: + yield current_value + i += 1 + current_value += i + + +def intsum2(): + 'generator to keep adding the next integer' + i = 0 + current_value = 0 + while True: + yield current_value + i += 1 + current_value += i + + +def doubler(): + 'generator to keep doubling the value' + current_value = 1 + while True: + yield current_value + current_value *= 2 + + +def fib(): + 'finonacci series generator' + a = 1 + b = 1 + while True: + yield a + a, b = b, a + b + + +def prime(): + 'generate all prime numbers between 2-100' + while True: + for i in range(2, 101): + prime = True + for x in range(2, i): + if i % x == 0: + prime = False + if prime is True: + yield i diff --git a/Student/rdrovdahl/lesson01/iterator.py b/Student/rdrovdahl/lesson01/iterator.py new file mode 100644 index 0000000..bbf712b --- /dev/null +++ b/Student/rdrovdahl/lesson01/iterator.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python + +""" +Simple iterator examples +""" + + +class IterateMe_1: + """ + About as simple an iterator as you can get: + + returns the sequence of numbers from zero to 4 + ( like range(4) ) + """ + + def __init__(self, stop=5): + self.current = -1 + self.stop = stop + + def __iter__(self): + return self + + def __next__(self): + self.current += 1 + if self.current < self.stop: + return self.current + else: + raise StopIteration + + +class InterateMe_2: + ''' + Adjusting iterator to behave more like the range() function by adding + start, stop, and step arguments + ''' + + def __init__(self, start, stop, step=1): + self.current = start - step + self.start = start + self.stop = stop + self.step = step + + def __iter__(self): + return self + + def __next__(self): + self.current += self.step + if self.current < self.stop: + return self.current + else: + raise StopIteration + + +if __name__ == "__main__": + + print("Testing the iterator") + for i in IterateMe_1(): + print(i) + + print('Testing interator#2') + it = InterateMe_2(2, 20, 2) + for i in it: + if i > 10: + break + print(i) + for i in it: + print(i) diff --git a/Student/rdrovdahl/lesson01/test_generator.py b/Student/rdrovdahl/lesson01/test_generator.py new file mode 100644 index 0000000..28f731d --- /dev/null +++ b/Student/rdrovdahl/lesson01/test_generator.py @@ -0,0 +1,60 @@ +""" +test_generator.py + +tests the solution to the generator lab + +can be run with py.test or nosetests +""" + +import generator_solution as gen + + +def test_intsum(): + + g = gen.intsum() + + assert next(g) == 0 + assert next(g) == 1 + assert next(g) == 3 + assert next(g) == 6 + assert next(g) == 10 + assert next(g) == 15 + + +def test_intsum2(): + + g = gen.intsum2() + + assert next(g) == 0 + assert next(g) == 1 + assert next(g) == 3 + assert next(g) == 6 + assert next(g) == 10 + assert next(g) == 15 + + +def test_doubler(): + + g = gen.doubler() + + assert next(g) == 1 + assert next(g) == 2 + assert next(g) == 4 + assert next(g) == 8 + assert next(g) == 16 + assert next(g) == 32 + + for i in range(10): + j = next(g) + assert j == 2**15 + + +def test_fib(): + g = gen.fib() + assert [next(g) for i in range(9)] == [1, 1, 2, 3, 5, 8, 13, 21, 34] + + +def test_prime(): + g = gen.prime() + for val in [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]: + assert next(g) == val diff --git a/Student/rdrovdahl/lesson01/top5.py b/Student/rdrovdahl/lesson01/top5.py new file mode 100644 index 0000000..8217448 --- /dev/null +++ b/Student/rdrovdahl/lesson01/top5.py @@ -0,0 +1,20 @@ +#! /usr/local/bin/python3 + + +''' +use pandas to scrub top 100 csv file and find songs with with danceability +scores over 0.8 and loundess scores under -5.0 +''' + +import pandas as pd + +music = pd.read_csv("featuresdf.csv") + +l1 = [(b,a,c,d) for a,b,c,d in zip(music.name, music.artists, music.danceability, music.loudness) if c > 0.8 and d < -5.0] +l2 = sorted(l1, key=lambda danceability: danceability[2], reverse=True) + +print('\n\nTop 5 - Quiet Dance Songs\n Sorted by Danceability:') + +for c, line in enumerate(l2, 1): + if c <= 5: + print(c, line[0] + ' --> ' + line[1]) diff --git a/Student/rdrovdahl/lesson02/Jupyter_lesson02.zip b/Student/rdrovdahl/lesson02/Jupyter_lesson02.zip new file mode 100644 index 0000000..f3d2146 Binary files /dev/null and b/Student/rdrovdahl/lesson02/Jupyter_lesson02.zip differ diff --git a/Student/rdrovdahl/lesson02/favorite_tracks_generator.py b/Student/rdrovdahl/lesson02/favorite_tracks_generator.py new file mode 100644 index 0000000..604bce5 --- /dev/null +++ b/Student/rdrovdahl/lesson02/favorite_tracks_generator.py @@ -0,0 +1,34 @@ +#! /usr/local/bin/python3 + + +''' +Write a generator to find and print all of your favorite tracks from the data +set. +''' + +import pandas as pd + +music = pd.read_csv("featuresdf.csv") + + +def fav_finder(artist): + 'generator to iterate over data set and return tracks from artist' + i = 0 + max = (len(music)-1) + while True: + if music.loc[i].artists == artist: + yield music.loc[i].artists, music.loc[i]['name'] + if i < max: + i += 1 + else: + break + + +fav = fav_finder('Imagine Dragons') + +while True: + try: + x = next(fav) + print(x[0], ' -- ', x[1]) + except StopIteration: + break diff --git a/Student/rdrovdahl/lesson02/featuresdf.csv b/Student/rdrovdahl/lesson02/featuresdf.csv new file mode 100644 index 0000000..ece51ea --- /dev/null +++ b/Student/rdrovdahl/lesson02/featuresdf.csv @@ -0,0 +1,101 @@ +id,name,artists,danceability,energy,key,loudness,mode,speechiness,acousticness,instrumentalness,liveness,valence,tempo,duration_ms,time_signature +7qiZfU4dY1lWllzX7mPBI,Shape of You,Ed Sheeran,0.825,0.652,1.0,-3.183,0.0,0.0802,0.581,0.0,0.0931,0.931,95.977,233713.0,4.0 +5CtI0qwDJkDQGwXD1H1cL,Despacito - Remix,Luis Fonsi,0.694,0.815,2.0,-4.328,1.0,0.12,0.229,0.0,0.0924,0.813,88.931,228827.0,4.0 +4aWmUDTfIPGksMNLV2rQP,Despacito (Featuring Daddy Yankee),Luis Fonsi,0.66,0.786,2.0,-4.757,1.0,0.17,0.209,0.0,0.112,0.846,177.833,228200.0,4.0 +6RUKPb4LETWmmr3iAEQkt,Something Just Like This,The Chainsmokers,0.617,0.635,11.0,-6.769,0.0,0.0317,0.0498,1.44e-05,0.164,0.446,103.019,247160.0,4.0 +3DXncPQOG4VBw3QHh3S81,I'm the One,DJ Khaled,0.609,0.668,7.0,-4.284,1.0,0.0367,0.0552,0.0,0.167,0.811,80.924,288600.0,4.0 +7KXjTSCq5nL1LoYtL7XAw,HUMBLE.,Kendrick Lamar,0.904,0.611,1.0,-6.842,0.0,0.0888,0.000259,2.03e-05,0.0976,0.4,150.02,177000.0,4.0 +3eR23VReFzcdmS7TYCrhC,It Ain't Me (with Selena Gomez),Kygo,0.64,0.533,0.0,-6.596,1.0,0.0706,0.119,0.0,0.0864,0.515,99.968,220781.0,4.0 +3B54sVLJ402zGa6Xm4YGN,Unforgettable,French Montana,0.726,0.769,6.0,-5.043,1.0,0.123,0.0293,0.0101,0.104,0.733,97.985,233902.0,4.0 +0KKkJNfGyhkQ5aFogxQAP,That's What I Like,Bruno Mars,0.853,0.56,1.0,-4.961,1.0,0.0406,0.013,0.0,0.0944,0.86,134.066,206693.0,4.0 +3NdDpSvN911VPGivFlV5d,"I Don’t Wanna Live Forever (Fifty Shades Darker) - From ""Fifty Shades Darker (Original Motion Picture Soundtrack)""",ZAYN,0.735,0.451,0.0,-8.374,1.0,0.0585,0.0631,1.3e-05,0.325,0.0862,117.973,245200.0,4.0 +7GX5flRQZVHRAGd6B4TmD,XO TOUR Llif3,Lil Uzi Vert,0.732,0.75,11.0,-6.366,0.0,0.231,0.00264,0.0,0.109,0.401,155.096,182707.0,4.0 +72jbDTw1piOOj770jWNea,Paris,The Chainsmokers,0.653,0.658,2.0,-6.428,1.0,0.0304,0.0215,1.66e-06,0.0939,0.219,99.99,221507.0,4.0 +0dA2Mk56wEzDgegdC6R17,Stay (with Alessia Cara),Zedd,0.679,0.634,5.0,-5.024,0.0,0.0654,0.232,0.0,0.115,0.498,102.013,210091.0,4.0 +4iLqG9SeJSnt0cSPICSjx,Attention,Charlie Puth,0.774,0.626,3.0,-4.432,0.0,0.0432,0.0969,3.12e-05,0.0848,0.777,100.041,211475.0,4.0 +0VgkVdmE4gld66l8iyGjg,Mask Off,Future,0.833,0.434,2.0,-8.795,1.0,0.431,0.0102,0.0219,0.165,0.281,150.062,204600.0,4.0 +3a1lNhkSLSkpJE4MSHpDu,Congratulations,Post Malone,0.627,0.812,6.0,-4.215,1.0,0.0358,0.198,0.0,0.212,0.504,123.071,220293.0,4.0 +6kex4EBAj0WHXDKZMEJaa,Swalla (feat. Nicki Minaj & Ty Dolla $ign),Jason Derulo,0.696,0.817,1.0,-3.862,1.0,0.109,0.075,0.0,0.187,0.782,98.064,216409.0,4.0 +6PCUP3dWmTjcTtXY02oFd,Castle on the Hill,Ed Sheeran,0.461,0.834,2.0,-4.868,1.0,0.0989,0.0232,1.14e-05,0.14,0.471,135.007,261154.0,4.0 +5knuzwU65gJK7IF5yJsua,Rockabye (feat. Sean Paul & Anne-Marie),Clean Bandit,0.72,0.763,9.0,-4.068,0.0,0.0523,0.406,0.0,0.18,0.742,101.965,251088.0,4.0 +0CcQNd8CINkwQfe1RDtGV,Believer,Imagine Dragons,0.779,0.787,10.0,-4.305,0.0,0.108,0.0524,0.0,0.14,0.708,124.982,204347.0,4.0 +2rb5MvYT7ZIxbKW5hfcHx,Mi Gente,J Balvin,0.543,0.677,11.0,-4.915,0.0,0.0993,0.0148,6.21e-06,0.13,0.294,103.809,189440.0,4.0 +0tKcYR2II1VCQWT79i5Nr,Thunder,Imagine Dragons,0.6,0.81,0.0,-4.749,1.0,0.0479,0.00683,0.21,0.155,0.298,167.88,187147.0,4.0 +5uCax9HTNlzGybIStD3vD,Say You Won't Let Go,James Arthur,0.358,0.557,10.0,-7.398,1.0,0.059,0.695,0.0,0.0902,0.494,85.043,211467.0,4.0 +79cuOz3SPQTuFrp8WgftA,There's Nothing Holdin' Me Back,Shawn Mendes,0.857,0.8,2.0,-4.035,1.0,0.0583,0.381,0.0,0.0913,0.966,121.996,199440.0,4.0 +6De0lHrwBfPfrhorm9q1X,Me Rehúso,Danny Ocean,0.744,0.804,1.0,-6.327,1.0,0.0677,0.0231,0.0,0.0494,0.426,104.823,205715.0,4.0 +6D0b04NJIKfEMg040WioJ,Issues,Julia Michaels,0.706,0.427,8.0,-6.864,1.0,0.0879,0.413,0.0,0.0609,0.42,113.804,176320.0,4.0 +0afhq8XCExXpqazXczTSv,Galway Girl,Ed Sheeran,0.624,0.876,9.0,-3.374,1.0,0.1,0.0735,0.0,0.327,0.781,99.943,170827.0,4.0 +3ebXMykcMXOcLeJ9xZ17X,Scared to Be Lonely,Martin Garrix,0.584,0.54,1.0,-7.786,0.0,0.0576,0.0895,0.0,0.261,0.195,137.972,220883.0,4.0 +7BKLCZ1jbUBVqRi2FVlTV,Closer,The Chainsmokers,0.748,0.524,8.0,-5.599,1.0,0.0338,0.414,0.0,0.111,0.661,95.01,244960.0,4.0 +1x5sYLZiu9r5E43kMlt9f,Symphony (feat. Zara Larsson),Clean Bandit,0.707,0.629,0.0,-4.581,0.0,0.0563,0.259,1.6e-05,0.138,0.457,122.863,212459.0,4.0 +5GXAXm5YOmYT0kL5jHvYB,I Feel It Coming,The Weeknd,0.768,0.813,0.0,-5.94,0.0,0.128,0.427,0.0,0.102,0.579,92.994,269187.0,4.0 +5aAx2yezTd8zXrkmtKl66,Starboy,The Weeknd,0.681,0.594,7.0,-7.028,1.0,0.282,0.165,3.49e-06,0.134,0.535,186.054,230453.0,4.0 +1OAh8uOEOvTDqkKFsKksC,Wild Thoughts,DJ Khaled,0.671,0.672,0.0,-3.094,0.0,0.0688,0.0329,0.0,0.118,0.632,97.98,204173.0,4.0 +7tr2za8SQg2CI8EDgrdtN,Slide,Calvin Harris,0.736,0.795,1.0,-3.299,0.0,0.0545,0.498,1.21e-06,0.254,0.511,104.066,230813.0,4.0 +2ekn2ttSfGqwhhate0LSR,New Rules,Dua Lipa,0.771,0.696,9.0,-6.258,0.0,0.0755,0.00256,9.71e-06,0.179,0.656,116.054,208827.0,4.0 +5tz69p7tJuGPeMGwNTxYu,1-800-273-8255,Logic,0.629,0.572,5.0,-7.733,0.0,0.0387,0.57,0.0,0.192,0.386,100.015,250173.0,4.0 +7hDc8b7IXETo14hHIHdnh,Passionfruit,Drake,0.809,0.463,11.0,-11.377,1.0,0.0396,0.256,0.085,0.109,0.364,111.98,298941.0,4.0 +7wGoVu4Dady5GV0Sv4UIs,rockstar,Post Malone,0.577,0.522,5.0,-6.594,0.0,0.0984,0.13,9.03e-05,0.142,0.119,159.772,218320.0,4.0 +6EpRaXYhGOB3fj4V2uDkM,Strip That Down,Liam Payne,0.869,0.485,6.0,-5.595,1.0,0.0545,0.246,0.0,0.0765,0.527,106.028,204502.0,4.0 +3A7qX2QjDlPnazUsRk5y0,2U (feat. Justin Bieber),David Guetta,0.548,0.65,8.0,-5.827,0.0,0.0591,0.219,0.0,0.225,0.557,144.937,194897.0,4.0 +0tgVpDi06FyKpA1z0VMD4,Perfect,Ed Sheeran,0.599,0.448,8.0,-6.312,1.0,0.0232,0.163,0.0,0.106,0.168,95.05,263400.0,3.0 +78rIJddV4X0HkNAInEcYd,Call On Me - Ryan Riback Extended Remix,Starley,0.676,0.843,0.0,-4.068,1.0,0.0367,0.0623,0.000752,0.181,0.718,105.003,222041.0,4.0 +5bcTCxgc7xVfSaMV3RuVk,Feels,Calvin Harris,0.893,0.745,11.0,-3.105,0.0,0.0571,0.0642,0.0,0.0943,0.872,101.018,223413.0,4.0 +0NiXXAI876aGImAd6rTj8,Mama,Jonas Blue,0.746,0.793,11.0,-4.209,0.0,0.0412,0.11,0.0,0.0528,0.557,104.027,181615.0,4.0 +0qYTZCo5Bwh1nsUFGZP3z,Felices los 4,Maluma,0.755,0.789,5.0,-4.502,1.0,0.146,0.231,0.0,0.351,0.737,93.973,229849.0,4.0 +2EEeOnHehOozLq4aS0n6S,iSpy (feat. Lil Yachty),KYLE,0.746,0.653,7.0,-6.745,1.0,0.289,0.378,0.0,0.229,0.672,75.016,253107.0,4.0 +152lZdxL1OR0ZMW6KquMi,Location,Khalid,0.736,0.449,1.0,-11.462,0.0,0.425,0.33,0.000162,0.0898,0.326,80.126,219080.0,4.0 +6mICuAdrwEjh6Y6lroV2K,Chantaje,Shakira,0.852,0.773,8.0,-2.921,0.0,0.0776,0.187,3.05e-05,0.159,0.907,102.034,195840.0,4.0 +4Km5HrUvYTaSUfiSGPJeQ,Bad and Boujee (feat. Lil Uzi Vert),Migos,0.927,0.665,11.0,-5.313,1.0,0.244,0.061,0.0,0.123,0.175,127.076,343150.0,4.0 +0ofbQMrRDsUaVKq2mGLEA,Havana,Camila Cabello,0.768,0.517,7.0,-4.323,0.0,0.0312,0.186,3.8e-05,0.104,0.418,104.992,216897.0,4.0 +6HUnnBwYZqcED1eQztxMB,Solo Dance,Martin Jensen,0.744,0.836,6.0,-2.396,0.0,0.0507,0.0435,0.0,0.194,0.36,114.965,174933.0,4.0 +343YBumqHu19cGoGARUTs,Fake Love,Drake,0.927,0.488,9.0,-9.433,0.0,0.42,0.108,0.0,0.196,0.605,133.987,210937.0,4.0 +4pdPtRcBmOSQDlJ3Fk945,Let Me Love You,DJ Snake,0.476,0.718,8.0,-5.309,1.0,0.0576,0.0784,1.02e-05,0.122,0.142,199.864,205947.0,4.0 +3PEgB3fkiojxms35ntsTg,More Than You Know,Axwell /\ Ingrosso,0.644,0.743,5.0,-5.002,0.0,0.0355,0.034,0.0,0.257,0.544,123.074,203000.0,4.0 +1xznGGDReH1oQq0xzbwXa,One Dance,Drake,0.791,0.619,1.0,-5.886,1.0,0.0532,0.00784,0.00423,0.351,0.371,103.989,173987.0,4.0 +7nKBxz47S9SD79N086fuh,SUBEME LA RADIO,Enrique Iglesias,0.684,0.823,9.0,-3.297,0.0,0.0773,0.0744,0.0,0.111,0.647,91.048,208163.0,4.0 +1NDxZ7cFAo481dtYWdrUn,Pretty Girl - Cheat Codes X CADE Remix,Maggie Lindemann,0.703,0.868,7.0,-4.661,0.0,0.0291,0.15,0.132,0.104,0.733,121.03,193613.0,4.0 +3m660poUr1chesgkkjQM7,Sorry Not Sorry,Demi Lovato,0.704,0.633,11.0,-6.923,0.0,0.241,0.0214,0.0,0.29,0.863,144.021,203760.0,4.0 +3kxfsdsCpFgN412fpnW85,Redbone,Childish Gambino,0.743,0.359,1.0,-10.401,1.0,0.0794,0.199,0.00611,0.137,0.587,160.083,326933.0,4.0 +6b8Be6ljOzmkOmFslEb23,24K Magic,Bruno Mars,0.818,0.803,1.0,-4.282,1.0,0.0797,0.034,0.0,0.153,0.632,106.97,225983.0,4.0 +6HZILIRieu8S0iqY8kIKh,DNA.,Kendrick Lamar,0.637,0.514,1.0,-6.763,1.0,0.365,0.0047,0.0,0.094,0.402,139.931,185947.0,4.0 +3umS4y3uQDkqekNjVpiRU,El Amante,Nicky Jam,0.683,0.691,8.0,-5.535,1.0,0.0432,0.243,0.0,0.14,0.732,179.91,219507.0,4.0 +00lNx0OcTJrS3MKHcB80H,You Don't Know Me - Radio Edit,Jax Jones,0.876,0.669,11.0,-6.054,0.0,0.138,0.163,0.0,0.185,0.682,124.007,213947.0,4.0 +6520aj0B4FSKGVuKNsOCO,Chained To The Rhythm,Katy Perry,0.448,0.801,0.0,-5.363,1.0,0.165,0.0733,0.0,0.146,0.462,189.798,237734.0,4.0 +1louJpMmzEicAn7lzDalP,No Promises (feat. Demi Lovato),Cheat Codes,0.741,0.667,10.0,-5.445,1.0,0.134,0.0575,0.0,0.106,0.595,112.956,223504.0,4.0 +2QbFClFyhMMtiurUjuQlA,Don't Wanna Know (feat. Kendrick Lamar),Maroon 5,0.775,0.617,7.0,-6.166,1.0,0.0701,0.341,0.0,0.0985,0.485,100.048,214265.0,4.0 +5hYTyyh2odQKphUbMqc5g,"How Far I'll Go - From ""Moana""",Alessia Cara,0.314,0.555,9.0,-9.601,1.0,0.37,0.157,0.000108,0.067,0.159,179.666,175517.0,4.0 +38yBBH2jacvDxrznF7h08,Slow Hands,Niall Horan,0.734,0.418,0.0,-6.678,1.0,0.0425,0.0129,0.0,0.0579,0.868,85.909,188174.0,4.0 +2cnKEkpVUSV4wnjQiTWfH,Escápate Conmigo,Wisin,0.747,0.864,8.0,-3.181,0.0,0.0599,0.0245,4.46e-05,0.0853,0.754,92.028,232787.0,4.0 +0SGkqnVQo9KPytSri1H6c,Bounce Back,Big Sean,0.77,0.567,2.0,-5.698,1.0,0.175,0.105,0.0,0.125,0.26,81.477,222360.0,4.0 +5Ohxk2dO5COHF1krpoPig,Sign of the Times,Harry Styles,0.516,0.595,5.0,-4.63,1.0,0.0313,0.0275,0.0,0.109,0.222,119.972,340707.0,4.0 +6gBFPUFcJLzWGx4lenP6h,goosebumps,Travis Scott,0.841,0.728,7.0,-3.37,1.0,0.0484,0.0847,0.0,0.149,0.43,130.049,243837.0,4.0 +5Z3GHaZ6ec9bsiI5Benrb,Young Dumb & Broke,Khalid,0.798,0.539,1.0,-6.351,1.0,0.0421,0.199,1.66e-05,0.165,0.394,136.949,202547.0,4.0 +6jA8HL9i4QGzsj6fjoxp8,There for You,Martin Garrix,0.611,0.644,6.0,-7.607,0.0,0.0553,0.124,0.0,0.124,0.13,105.969,221904.0,4.0 +21TdkDRXuAB3k90ujRU1e,Cold (feat. Future),Maroon 5,0.697,0.716,9.0,-6.288,0.0,0.113,0.118,0.0,0.0424,0.506,99.905,234308.0,4.0 +7vGuf3Y35N4wmASOKLUVV,Silence,Marshmello,0.52,0.761,4.0,-3.093,1.0,0.0853,0.256,4.96e-06,0.17,0.286,141.971,180823.0,4.0 +1mXVgsBdtIVeCLJnSnmtd,Too Good At Goodbyes,Sam Smith,0.698,0.375,5.0,-8.279,1.0,0.0491,0.652,0.0,0.173,0.534,91.92,201000.0,4.0 +3EmmCZoqpWOTY1g2GBwJo,Just Hold On,Steve Aoki,0.647,0.932,11.0,-3.515,1.0,0.0824,0.00383,1.5e-06,0.0574,0.374,114.991,198774.0,4.0 +6uFsE1JgZ20EXyU0JQZbU,Look What You Made Me Do,Taylor Swift,0.773,0.68,9.0,-6.378,0.0,0.141,0.213,1.57e-05,0.122,0.497,128.062,211859.0,4.0 +0CokSRCu5hZgPxcZBaEzV,Glorious (feat. Skylar Grey),Macklemore,0.731,0.794,0.0,-5.126,0.0,0.0522,0.0323,2.59e-05,0.112,0.356,139.994,220454.0,4.0 +6875MeXyCW0wLyT72Eetm,Starving,Hailee Steinfeld,0.721,0.626,4.0,-4.2,1.0,0.123,0.402,0.0,0.102,0.558,99.914,181933.0,4.0 +3AEZUABDXNtecAOSC1qTf,Reggaetón Lento (Bailemos),CNCO,0.761,0.838,4.0,-3.073,0.0,0.0502,0.4,0.0,0.176,0.71,93.974,222560.0,4.0 +3E2Zh20GDCR9B1EYjfXWy,Weak,AJR,0.673,0.637,5.0,-4.518,1.0,0.0429,0.137,0.0,0.184,0.678,123.98,201160.0,4.0 +4pLwZjInHj3SimIyN9SnO,Side To Side,Ariana Grande,0.648,0.738,6.0,-5.883,0.0,0.247,0.0408,0.0,0.292,0.603,159.145,226160.0,4.0 +3QwBODjSEzelZyVjxPOHd,Otra Vez (feat. J Balvin),Zion & Lennox,0.832,0.772,10.0,-5.429,1.0,0.1,0.0559,0.000486,0.44,0.704,96.016,209453.0,4.0 +1wjzFQodRWrPcQ0AnYnvQ,I Like Me Better,Lauv,0.752,0.505,9.0,-7.621,1.0,0.253,0.535,2.55e-06,0.104,0.419,91.97,197437.0,4.0 +04DwTuZ2VBdJCCC5TROn7,In the Name of Love,Martin Garrix,0.49,0.485,4.0,-6.237,0.0,0.0406,0.0592,0.0,0.337,0.196,133.889,195840.0,4.0 +6DNtNfH8hXkqOX1sjqmI7,Cold Water (feat. Justin Bieber & MØ),Major Lazer,0.608,0.798,6.0,-5.092,0.0,0.0432,0.0736,0.0,0.156,0.501,92.943,185352.0,4.0 +1UZOjK1BwmwWU14Erba9C,Malibu,Miley Cyrus,0.573,0.781,8.0,-6.406,1.0,0.0555,0.0767,2.64e-05,0.0813,0.343,139.934,231907.0,4.0 +4b4KcovePX8Ke2cLIQTLM,All Night,The Vamps,0.544,0.809,8.0,-5.098,1.0,0.0363,0.0038,0.0,0.323,0.448,145.017,197640.0,4.0 +1a5Yu5L18qNxVhXx38njO,Hear Me Now,Alok,0.789,0.442,11.0,-7.844,1.0,0.0421,0.586,0.00366,0.0927,0.45,121.971,192846.0,4.0 +4c2W3VKsOFoIg2SFaO6DY,Your Song,Rita Ora,0.855,0.624,1.0,-4.093,1.0,0.0488,0.158,0.0,0.0513,0.962,117.959,180757.0,4.0 +22eADXu8DfOAUEDw4vU8q,Ahora Dice,Chris Jeday,0.708,0.693,6.0,-5.516,1.0,0.138,0.246,0.0,0.129,0.427,143.965,271080.0,4.0 +7nZmah2llfvLDiUjm0kiy,Friends (with BloodPop®),Justin Bieber,0.744,0.739,8.0,-5.35,1.0,0.0387,0.00459,0.0,0.306,0.649,104.99,189467.0,4.0 +2fQrGHiQOvpL9UgPvtYy6,Bank Account,21 Savage,0.884,0.346,8.0,-8.228,0.0,0.351,0.0151,7.04e-06,0.0871,0.376,75.016,220307.0,4.0 +1PSBzsahR2AKwLJgx8ehB,Bad Things (with Camila Cabello),Machine Gun Kelly,0.675,0.69,2.0,-4.761,1.0,0.132,0.21,0.0,0.287,0.272,137.817,239293.0,4.0 +0QsvXIfqM0zZoerQfsI9l,Don't Let Me Down,The Chainsmokers,0.542,0.859,11.0,-5.651,1.0,0.197,0.16,0.00466,0.137,0.403,159.797,208053.0,4.0 +7mldq42yDuxiUNn08nvzH,Body Like A Back Road,Sam Hunt,0.731,0.469,5.0,-7.226,1.0,0.0326,0.463,1.04e-06,0.103,0.631,98.963,165387.0,4.0 +7i2DJ88J7jQ8K7zqFX2fW,Now Or Never,Halsey,0.658,0.588,6.0,-4.902,0.0,0.0367,0.105,1.28e-06,0.125,0.434,110.075,214802.0,4.0 +1j4kHkkpqZRBwE0A4CN4Y,Dusk Till Dawn - Radio Edit,ZAYN,0.258,0.437,11.0,-6.593,0.0,0.039,0.101,1.27e-06,0.106,0.0967,180.043,239000.0,4.0 diff --git a/Student/rdrovdahl/lesson02/high_engergy_tracks_closure.py b/Student/rdrovdahl/lesson02/high_engergy_tracks_closure.py new file mode 100644 index 0000000..c844b01 --- /dev/null +++ b/Student/rdrovdahl/lesson02/high_engergy_tracks_closure.py @@ -0,0 +1,22 @@ +#! /usr/local/bin/python3 + + +''' +Write a closure to find and print all 'high energy' tracks from the data +set. +''' + +import pandas as pd + + +def music_filter(): + music = pd.read_csv("featuresdf.csv") + + def high_energy(val): + nonlocal music + return music[music.energy > val][['artists', 'name', 'energy']] + return high_energy + + +energy_filter = music_filter() +print(energy_filter(0.8)) diff --git a/Student/rdrovdahl/lesson03/locke.py b/Student/rdrovdahl/lesson03/locke.py new file mode 100644 index 0000000..69ef76f --- /dev/null +++ b/Student/rdrovdahl/lesson03/locke.py @@ -0,0 +1,242 @@ +#! /usr/local/bin/python3 + + +''' +Write a context manager for a simulated ballard locke program +There are 2 lockes - one small which holds up to 5 boats and one large which +holds up to 10 boats. + +When a locke is entered, you need to: + Stop the pumps + Open the doors + Close the doors + Restart the pumps + Transfer boats + etc... + +During initialization the context manger class accepts the locke’s capacity in +number of boats. If the locke's capacity is exceeded, raise a suitable error. + +For this simulation, we will only need to print the system controls commands. +''' + +from time import sleep, time +import datetime +system_status = True +start_time = 0 + + +class locke_enter(): + def __init__(self, boats, size, handle_error=True): + global start_time + self.boats = boats + self.handle_error = handle_error + self.size = size + self.max = 0 + if self.size == 'small': + self.max = 5 + if self.size == 'large': + self.max = 10 + start_time = time() + + def __enter__(self): + print('\n' + ('_' * 80 + '\n') * 2) + self.now = datetime.datetime.now() + self.time = str(self.now.strftime("%H") + ':' + self.now.strftime("%M") + ':' + self.now.strftime("%S")) + print(f'{self.boats} boats would like to traverse the {self.size} locke at {self.time}') + if self.boats > self.max: + raise Exception(f'Too many boats for {self.size} locke') + self.doors_open() + return self + + def __exit__(self, type, value, traceback): + global system_status + if traceback: + print('\n\n ___SYSTEM FAILURE___') + print(' type: {}'.format(type)) + print(' value: {}'.format(value)) + print(' traceback: {}'.format(traceback)) + print('\n shutting down the system...') + system_status = False + print(f' {self.boats} boats have been stranded!\n') + return self.handle_error + + def doors_open(self): + print('opening the doors...', end='', flush=True) + sleep(2) + print('doors open') + sleep(1) + + def doors_close(self): + print('closing the doors...', end='', flush=True) + sleep(2) + print('doors closed') + sleep(1) + + def boats_enter(self): + print('boats entering the locke...') + sleep(2) + for _ in range(1, self.boats + 1): + print(f' boat {_} has entered the locke') + sleep(2) + print('all boats are in the locke') + sleep(1) + + +class locke_pump(): + def __init__(self, boats, handle_error=True): + self.boats = boats + self.handle_error = handle_error + + def __enter__(self): + self.start = time() + return self + + def __exit__(self, type, value, traceback): + global system_status + if traceback: + print('\n\n ___SYSTEM FAILURE___') + print(' type: {}'.format(type)) + print(' value: {}'.format(value)) + print(' traceback: {}'.format(traceback)) + print('\n shutting down the system...') + system_status = False + print(f' {self.boats} boats have been stranded!\n') + return self.handle_error + else: + self.pump_stop() + + def pump_start(self): + print('starting pumps...', end='', flush=True) + sleep(2) + print('pumps started') + sleep(1) + + def pump_stop(self): + print('stopping the pumps...', end='', flush=True) + sleep(2) + print('pumps stopped') + sleep(1) + + +class locke_exit(): + global start_time + + def __init__(self, boats, handle_error=True): + self.boats = boats + self.handle_error = handle_error + + def __enter__(self): + self.start = time() + self.doors_open() + return self + + def __exit__(self, type, value, traceback): + global system_status + if traceback: + print('\n\n ___SYSTEM FAILURE___') + print(' type: {}'.format(type)) + print(' value: {}'.format(value)) + print(' traceback: {}'.format(traceback)) + print('\n shutting down the system...') + system_status = False + print(f' {self.boats} boats have been stranded!\n') + return self.handle_error + else: + self.doors_close() + self.now = datetime.datetime.now() + self.time = str(self.now.strftime("%H") + ':' + self.now.strftime("%M") + ':' + self.now.strftime("%S")) + self.end = time() + total_time = self.end - start_time + print(f'{self.boats} boats have finished traversing the locke at {self.time}') + print(f'Total traversal time: {total_time:.2f} seconds') + + def doors_open(self): + print('opening the doors...', end='', flush=True) + sleep(2) + print('doors open') + sleep(1) + + def doors_close(self): + print('closing the doors...', end='', flush=True) + sleep(2) + print('doors closed') + sleep(1) + + def boats_exit(self): + print('boats exiting the locke...') + sleep(2) + for _ in range(1, self.boats + 1): + print(f' boat {_} has exited the locke') + sleep(2) + print('all boats have exited the locke') + sleep(2) + + +def through_the_locke(boats, size='small', handle_error=True): + # size can be 'small' for up to 5 boats or 'large' for up to 10 boats + global system_status + if system_status is True: + try: + with locke_enter(boats, size, handle_error) as x: + x.boats_enter() + except Exception as e: + print(e) + return + if system_status is True: + with locke_pump(boats, handle_error) as x: + x.pump_start() + if system_status is True: + with locke_exit(boats, handle_error) as x: + x.boats_exit() + if system_status is not True: + print('**** locke is offline for repairs ****') + + +def through_the_locke_with_error(boats, size='small', handle_error=True): + # this function simulates a RuntimeError during the pumping procedure + # the error should be handled in the context manager class and global + # system_status variable will be changed to shut down the system + global system_status + if system_status is True: + try: + with locke_enter(boats, size, handle_error) as x: + x.boats_enter() + except Exception as e: + print(e) + return + if system_status is True: + with locke_pump(boats, handle_error) as y: + y.pump_start() + raise RuntimeError('major pump malfunction detected') + if system_status is True: + with locke_exit(boats, handle_error, 'small') as z: + z.boats_exit() + if system_status is not True: + print('**** locke is offline for repairs ****') + + +def main(): + # run through a series of locke traversals + + # this call will send 4 boats through the small locke + through_the_locke(4, 'small') + + # this call will try to send 8 boats through the small locke but fail + # because the small locke only takes up to 5 boats + through_the_locke(8, 'small') + + # this call will try to send 12 boats through the large locke but fail + # because the large locke only takes up to 10 boats + through_the_locke(12, 'large') + + # this call will send 8 boats through the large locke + through_the_locke(8, 'large') + + # this call shows how error handling works when an error is raised within + # the context + through_the_locke_with_error(3, 'small') + + +if __name__ == "__main__": + main() diff --git a/Student/rdrovdahl/lesson03/recursion.py b/Student/rdrovdahl/lesson03/recursion.py new file mode 100644 index 0000000..83ddc62 --- /dev/null +++ b/Student/rdrovdahl/lesson03/recursion.py @@ -0,0 +1,15 @@ +#! /usr/local/bin/python3 + + +'Write a recursive solution for the factorial function.' + + +def factorial(num): + if num == 1: + return 1 + else: + f = num * factorial(num-1) + return f + +x = factorial(10) +print(x) diff --git a/Student/rdrovdahl/lesson04/MangledSingleton.py b/Student/rdrovdahl/lesson04/MangledSingleton.py new file mode 100644 index 0000000..51d749e --- /dev/null +++ b/Student/rdrovdahl/lesson04/MangledSingleton.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 + +''' +create a new metaclass that combines the Singleton and Mangler classes + +“The singleton pattern is a software design pattern that restricts the +instantiation of a class to one object.” + +''' + + +class MangledSinglton(type): + instance = None + + # this is the code that creates custom attributes for the new class + def __new__(cls, clsname, bases, _dict): + uppercase_attr = {} + for name, val in _dict.items(): + if not name.startswith('__'): + uppercase_attr[name.upper()] = val + uppercase_attr[name.upper()*2] = val + uppercase_attr[name.lower()] = val + uppercase_attr[name.lower()*2] = val + else: + uppercase_attr[name] = val + return super().__new__(cls, clsname, bases, uppercase_attr) + + # this is the Singleton code which restricts the instantiation of a class + # to one object + def __call__(cls, *args, **kwargs): + if cls.instance is None: + cls.instance = super().__call__(*args, **kwargs) + return cls.instance + + + + +class MyClass(metaclass=MangledSinglton): + x = 1 + +o1 = MyClass() +o2 = MyClass() +print(o1.X) +assert id(o1) == id(o2) +print(bool(id(o1) == id(o2))) diff --git a/Student/rdrovdahl/lesson04/cool_meta.py b/Student/rdrovdahl/lesson04/cool_meta.py new file mode 100644 index 0000000..ef3e821 --- /dev/null +++ b/Student/rdrovdahl/lesson04/cool_meta.py @@ -0,0 +1,30 @@ +""" +Complete do-nothing metaclass example + +It serves to show when each special method of the metaclass is called. + +""" + + +class CoolMeta(type): + def __new__(meta, name, bases, dct): + print('Creating class in CoolMeta.__new__', name) + return super().__new__(meta, name, bases, dct) + + def __init__(cls, name, bases, dct): + print('Initializing class in CoolMeta.__init__', name) + super().__init__(name, bases, dct) + + def __call__(cls, *args, **kw): + print('calling CoolMeta to instantiate ', cls) + return type.__call__(cls, *args, **kw) + + +class CoolClass(metaclass=CoolMeta): + def __init__(self): + print('And now my CoolClass object exists') + + +print('everything loaded, instantiate a CoolClass instance now') + +foo = CoolClass() diff --git a/Student/rdrovdahl/lesson04/get_set_attr.py b/Student/rdrovdahl/lesson04/get_set_attr.py new file mode 100644 index 0000000..d0d8ab2 --- /dev/null +++ b/Student/rdrovdahl/lesson04/get_set_attr.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 + +""" +Manipulating attributes + +Example code for manipulating attributes +""" + + +# A simple class for a person +class Person: + def __init__(self, first_name="", last_name="", phone=""): + self.first_name = first_name + self.last_name = last_name + self.phone = phone + + def __str__(self): + msg = ["Person:"] + for name, val in vars(self).items(): + msg.append("{}: {}".format(name, val)) + return "\n".join(msg) + + +def update_person(person): + while True: + att = input("What would you like to update for:\n" + "{}\n" + '(type "quit" to quit) >> '.format(person) + ) + if att.strip().lower() == "quit": + break + if not hasattr(person, att): + ans = input("This person does not have that attribute.\n" + "Would you like to add it? Y,[N] >> ") + if not ans.lower().startswith('y'): + continue + else: + ans = input("What would you like to set it to? >> ") + setattr(person, att, ans) + # new code to allow deleting of attributes + elif hasattr(person, att): + ans = input('This attribute already exists. \n' + 'Would you like to delete it? Y, [N] >>') + if not ans.lower().startswith('y'): + continue + else: + delattr(person, att) + + +if __name__ == "__main__": + # a little test code: + + # create a couple people: + p1 = Person("Fred", "Jones", "206-555-1234") + update_person(p1) diff --git a/Student/rdrovdahl/lesson04/json_save/.cache/v/cache/lastfailed b/Student/rdrovdahl/lesson04/json_save/.cache/v/cache/lastfailed new file mode 100644 index 0000000..051b538 --- /dev/null +++ b/Student/rdrovdahl/lesson04/json_save/.cache/v/cache/lastfailed @@ -0,0 +1,12 @@ +{ + "json_save/test/test_savables.py::test_bad_dicts[val4]": true, + "json_save/test/test_savables.py::test_basics": true, + "json_save/test/test_savables.py::test_basics[Tuple-val5]": true, + "json_save/test/test_savables.py::test_containers[Dict-val0]": true, + "json_save/test/test_savables.py::test_dicts[Dict-val0]": true, + "json_save/test/test_savables.py::test_dicts[Dict-val1]": true, + "json_save/test/test_savables.py::test_dicts[Dict-val2]": true, + "json_save/test/test_savables.py::test_dicts[Dict-val3]": true, + "json_save/test/test_savables.py::test_dicts[val10]": true, + "json_save/test/test_savables.py::test_dicts[val7]": true +} \ No newline at end of file diff --git a/Student/rdrovdahl/lesson04/json_save/README.txt b/Student/rdrovdahl/lesson04/json_save/README.txt new file mode 100644 index 0000000..a80b252 --- /dev/null +++ b/Student/rdrovdahl/lesson04/json_save/README.txt @@ -0,0 +1,4 @@ +This is a simple meta-class based system for saving objects in the JSON format. + +It can make any arbitrary class savable and re-loadable from JSON. + diff --git a/Student/rdrovdahl/lesson04/json_save/examples/example_dec.py b/Student/rdrovdahl/lesson04/json_save/examples/example_dec.py new file mode 100755 index 0000000..263b6c7 --- /dev/null +++ b/Student/rdrovdahl/lesson04/json_save/examples/example_dec.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python + +""" +Examples of using json_save +""" + +import json_save.json_save_dec as js + + +# Examples using the decorator + +@js.json_save +class MyClass: + + x = js.Int() + y = js.Float() + lst = js.List() + + def __init__(self, x, lst): + self.x = x + self.lst = lst + + +@js.json_save +class OtherSaveable: + + foo = js.String() + bar = js.Int() + + def __init__(self, foo, bar): + self.foo = foo + self.bar = bar + + +# create one: +print("about to create a instance") +mc = MyClass(5, [3, 5, 7, 9]) + +print(mc) + +jc = mc.to_json_compat() + +# re-create it from the dict: +mc2 = MyClass.from_json_dict(jc) + +print(mc2 == "fred") + +assert mc2 == mc + +print(mc.to_json()) + +# now try it nested... +mc_nest = MyClass(34, [OtherSaveable("this", 2), + OtherSaveable("that", 64), + ]) + +mc_nest_comp = mc_nest.to_json_compat() +print(mc_nest_comp) + +# can we re-create it? +mc_nest2 = MyClass.from_json_dict(mc_nest_comp) + +print(mc_nest) +print(mc_nest2) + +assert mc_nest == mc_nest2 diff --git a/Student/rdrovdahl/lesson04/json_save/examples/example_meta.py b/Student/rdrovdahl/lesson04/json_save/examples/example_meta.py new file mode 100755 index 0000000..13719ab --- /dev/null +++ b/Student/rdrovdahl/lesson04/json_save/examples/example_meta.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python + +""" +Examples of using json_save +""" + +import json_save.json_save_meta as js + +# Metaclass examples + +class MyClass(js.JsonSaveable): + + x = js.Int() + y = js.Float() + lst = js.List() + + def __init__(self, x, lst): + self.x = x + self.lst = lst + + +class OtherSaveable(js.JsonSaveable): + + foo = js.String() + bar = js.Int() + + def __init__(self, foo, bar): + self.foo = foo + self.bar = bar + +# create one: +print("about to create a instance") +mc = MyClass(5, [3, 5, 7, 9]) + +print(mc) + +jc = mc.to_json_compat() + +# re-create it from the dict: +mc2 = MyClass.from_json_dict(jc) + +print(mc2 == "fred") + +assert mc2 == mc + +print(mc.to_json()) + +# now try it nested... +mc_nest = MyClass(34, [OtherSaveable("this", 2), + OtherSaveable("that", 64), + ]) + +mc_nest_comp = mc_nest.to_json_compat() +print(mc_nest_comp) + +# can we re-create it? +mc_nest2 = MyClass.from_json_dict(mc_nest_comp) + +print(mc_nest) +print(mc_nest2) + +assert mc_nest == mc_nest2 + diff --git a/Student/rdrovdahl/lesson04/json_save/json_save/__init__.py b/Student/rdrovdahl/lesson04/json_save/json_save/__init__.py new file mode 100644 index 0000000..db59bd9 --- /dev/null +++ b/Student/rdrovdahl/lesson04/json_save/json_save/__init__.py @@ -0,0 +1,8 @@ +""" +json_save package + +Pulling in names from the other packages. +""" + +__version__ = "0.4.0" + diff --git a/Student/rdrovdahl/lesson04/json_save/json_save/__pycache__/__init__.cpython-36.pyc b/Student/rdrovdahl/lesson04/json_save/json_save/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000..14b2d0c Binary files /dev/null and b/Student/rdrovdahl/lesson04/json_save/json_save/__pycache__/__init__.cpython-36.pyc differ diff --git a/Student/rdrovdahl/lesson04/json_save/json_save/__pycache__/json_save_dec.cpython-36.pyc b/Student/rdrovdahl/lesson04/json_save/json_save/__pycache__/json_save_dec.cpython-36.pyc new file mode 100644 index 0000000..ebd80d0 Binary files /dev/null and b/Student/rdrovdahl/lesson04/json_save/json_save/__pycache__/json_save_dec.cpython-36.pyc differ diff --git a/Student/rdrovdahl/lesson04/json_save/json_save/__pycache__/json_save_meta.cpython-36.pyc b/Student/rdrovdahl/lesson04/json_save/json_save/__pycache__/json_save_meta.cpython-36.pyc new file mode 100644 index 0000000..6a93ea5 Binary files /dev/null and b/Student/rdrovdahl/lesson04/json_save/json_save/__pycache__/json_save_meta.cpython-36.pyc differ diff --git a/Student/rdrovdahl/lesson04/json_save/json_save/__pycache__/saveables.cpython-36.pyc b/Student/rdrovdahl/lesson04/json_save/json_save/__pycache__/saveables.cpython-36.pyc new file mode 100644 index 0000000..3cce838 Binary files /dev/null and b/Student/rdrovdahl/lesson04/json_save/json_save/__pycache__/saveables.cpython-36.pyc differ diff --git a/Student/rdrovdahl/lesson04/json_save/json_save/json_save_dec.py b/Student/rdrovdahl/lesson04/json_save/json_save/json_save_dec.py new file mode 100644 index 0000000..5af4de9 --- /dev/null +++ b/Student/rdrovdahl/lesson04/json_save/json_save/json_save_dec.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python + +""" +json_save implemented as a decorator +""" + +import json +from pathlib import Path + +from .json_save_meta import * + + +# assorted methods that will need to be added to the decorated class: +def _to_json_compat(self): + """ + converts this object to a json-compatible dict. + + returns the dict + """ + # add and __obj_type attribute, so it can be reconstructed + dic = {"__obj_type": self.__class__.__qualname__} + for attr, typ in self._attrs_to_save.items(): + dic[attr] = typ.to_json_compat(getattr(self, attr)) + return dic + + +def __eq__(self, other): + """ + default equality method that checks if all of the saved attributes + are equal + """ + for attr in self._attrs_to_save: + try: + if getattr(self, attr) != getattr(other, attr): + return False + except AttributeError: + return False + return True + +@classmethod +def _from_json_dict(cls, dic): + """ + creates an instance of this class populated by the contents of + the json compatible dict + + the object is created with __new__ before setting the attributes + + NOTE: __init__ is not called. + There should not be any extra initialization required in __init__ + """ + # create a new object + obj = cls.__new__(cls) + for attr, typ in cls._attrs_to_save.items(): + setattr(obj, attr, typ.to_python(dic[attr])) + return obj + + +def __new__(cls, *args, **kwargs): + """ + This adds instance attributes to assure they are all there, even if + they are not set in the subclasses __init__ + + it's in __new__ so that it will get called before the decorated class' + __init__ -- the __init__ will override anything here. + """ + # create the instance by calling the base class __new__ + obj = cls.__base__.__new__(cls) + # using super() did not work here -- why?? + # set the instance attributes to defaults + for attr, typ in cls._attrs_to_save.items(): + setattr(obj, attr, typ.default) + return obj + + +def _to_json(self, fp=None, indent=4): + """ + Converts the object to JSON + + :param fp=None: an open file_like object to write the json to. + If it is None, then a string with the JSON + will be returned as a string + + :param indent=4: The indentation level desired in the JSON + """ + if fp is None: + return json.dumps(self.to_json_compat(), indent=indent) + else: + json.dump(self.to_json_compat(), fp, indent=indent) + + +# now the actual decorator +def json_save(cls): + """ + json_save decorator + + makes decorated classes Saveable to json + """ + # make sure this is decorating a class object + if type(cls) is not type: + raise TypeError("json_save can only be used on classes") + + # find the saveable attributes + # these will the attributes that get saved and reconstructed from json. + # each class object gets its own dict + attr_dict = vars(cls) + cls._attrs_to_save = {} + for key, attr in attr_dict.items(): + if isinstance(attr, Saveable): + cls._attrs_to_save[key] = attr + if not cls._attrs_to_save: + raise TypeError(f"{cls.__name__} class has no saveable attributes.\n" + " Note that Savable attributes must be instances") + # register this class so we can re-construct instances. + Saveable.ALL_SAVEABLES[cls.__qualname__] = cls + + # add the methods: + cls.__new__ = __new__ + cls.to_json_compat = _to_json_compat + cls.__eq__ = __eq__ + cls.from_json_dict = _from_json_dict + cls.to_json = _to_json + + return cls + + +# utilities for loading arbitrary objects from json +def from_json_dict(j_dict): + """ + factory function that creates an arbitrary JsonSaveable + object from a json-compatible dict. + """ + # determine the class it is. + obj_type = j_dict["__obj_type"] + obj = Saveable.ALL_SAVEABLES[obj_type].from_json_dict(j_dict) + return obj + + +def from_json(_json): + """ + Factory function that re-creates a JsonSaveable object + from a json string or file + """ + if isinstance(_json, (str, Path)): + return from_json_dict(json.loads(_json)) + else: # assume a file-like object + return from_json_dict(json.load(_json)) diff --git a/Student/rdrovdahl/lesson04/json_save/json_save/json_save_meta.py b/Student/rdrovdahl/lesson04/json_save/json_save/json_save_meta.py new file mode 100644 index 0000000..5b8dfa8 --- /dev/null +++ b/Student/rdrovdahl/lesson04/json_save/json_save/json_save_meta.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 + +""" +json_save + +metaclass based system for saving objects in a JSON format + +This could be useful, but it's kept simple to show the use of metaclasses + +The idea is that you subclass from JsonSavable, and then you get an object +that be saved and reloaded to/from JSON +""" + +import json + +# import * is a bad idea in general, but helpful for a modules that's part +# of a package, where you control the names. +from .saveables import * + + +class MetaJsonSaveable(type): + """ + The metaclass for creating JsonSavable classes + + Deriving from type makes it a metaclass. + + Note: the __init__ gets run at compile time, not run time. + (module import time) + """ + def __init__(cls, name, bases, attr_dict): + # it gets the class object as the first param. + # and then the same parameters as the type() factory function + + # you want to call the regular type initilizer: + super().__init__(name, bases, attr_dict) + + # here's where we work with the class attributes: + # these will the attributes that get saved and reconstructed from json. + # each class object gets its own dict + cls._attrs_to_save = {} + for key, attr in attr_dict.items(): + if isinstance(attr, Saveable): + cls._attrs_to_save[key] = attr + # special case JsonSaveable -- no attrs to save yet + if cls.__name__ != "JsonSaveable" and (not cls._attrs_to_save): + raise TypeError(f"{cls.__name__} class has no saveable attributes.\n" + " Note that Savable attributes must be instances") + + # register this class so we can re-construct instances. + Saveable.ALL_SAVEABLES[attr_dict["__qualname__"]] = cls + + +class JsonSaveable(metaclass=MetaJsonSaveable): + """ + mixin for JsonSavable objects + """ + def __new__(cls, *args, **kwargs): + """ + This adds instance attributes to assure they are all there, even if + they are not set in the subclasses __init__ + """ + # create the instance + obj = super().__new__(cls) + # set the instance attributes to defaults + for attr, typ in cls._attrs_to_save.items(): + setattr(obj, attr, typ.default) + return obj + + def __eq__(self, other): + """ + default equality method that checks if all of the saved attributes + are equal + """ + for attr in self._attrs_to_save: + try: + if getattr(self, attr) != getattr(other, attr): + return False + except AttributeError: + return False + return True + + def to_json_compat(self): + """ + converts this object to a json-compatible dict. + + returns the dict + """ + # add and __obj_type attribute, so it can be reconstructed + dic = {"__obj_type": self.__class__.__qualname__} + for attr, typ in self._attrs_to_save.items(): + dic[attr] = typ.to_json_compat(getattr(self, attr)) + return dic + + @classmethod + def from_json_dict(cls, dic): + """ + creates an instance of this class populated by the contents of + the json compatible dict + + the object is created with __new__ before setting the attributes + + NOTE: __init__ is not called. + There should not be any extra initialization required in __init__ + """ + # create a new object + obj = cls.__new__(cls) + for attr, typ in cls._attrs_to_save.items(): + setattr(obj, attr, typ.to_python(dic[attr])) + # make sure it gets initialized + # obj.__init__() + return obj + + def to_json(self, fp=None, indent=4): + """ + Converts the object to JSON + + :param fp=None: an open file_like object to write the json to. + If it is None, then a string with the JSON + will be returned as a string + + :param indent=4: The indentation level desired in the JSON + """ + if fp is None: + return json.dumps(self.to_json_compat(), indent=indent) + else: + json.dump(self.to_json_compat(), fp, indent=indent) + + def __str__(self): + msg = ["{} object, with attributes:".format(self.__class__.__qualname__)] + for attr in self._attrs_to_save.keys(): + msg.append("{}: {}".format(attr, getattr(self, attr))) + return "\n".join(msg) + + +def from_json_dict(j_dict): + """ + factory function that creates an arbitrary JsonSavable + object from a json-compatible dict. + """ + # determine the class it is. + obj_type = j_dict["__obj_type"] + obj = Saveable.ALL_SAVEABLES[obj_type].from_json_dict(j_dict) + return obj + + +def from_json(_json): + """ + factory function that re-creates a JsonSavable object + from a json string or file + """ + if isinstance(_json, str): + return from_json_dict(json.loads(_json)) + else: # assume a file-like object + return from_json_dict(json.load(_json)) + + +if __name__ == "__main__": + + # Example of using it. + class MyClass(JsonSaveable): + + x = Int() + y = Float() + l = List() + + def __init__(self, x, lst): + self.x = x + self.lst = lst + + + class OtherSaveable(JsonSavable): + + foo = String() + bar = Int() + + def __init__(self, foo, bar): + self.foo = foo + self.bar = bar + + # create one: + print("about to create a instance") + mc = MyClass(5, [3, 5, 7, 9]) + + print(mc) + + jc = mc.to_json_compat() + + # re-create it from the dict: + mc2 = MyClass.from_json_dict(jc) + + print(mc2 == "fred") + + assert mc2 == mc + + print(mc.to_json()) + + # now try it nested... + mc_nest = MyClass(34, [OtherSaveable("this", 2), + OtherSaveable("that", 64), + ]) + + mc_nest_comp = mc_nest.to_json_compat() + print(mc_nest_comp) + + # can we re-create it? + mc_nest2 = MyClass.from_json_dict(mc_nest_comp) + + print(mc_nest) + print(mc_nest2) + + assert mc_nest == mc_nest2 diff --git a/Student/rdrovdahl/lesson04/json_save/json_save/saveables.py b/Student/rdrovdahl/lesson04/json_save/json_save/saveables.py new file mode 100644 index 0000000..16ac75f --- /dev/null +++ b/Student/rdrovdahl/lesson04/json_save/json_save/saveables.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python + +""" +The Saveable objects used by both the metaclass and decorator approach. +""" +import ast + +# import json + +__all__ = ['Bool', + 'Dict', + 'Float', + 'Int', + 'List', + 'Saveable', + 'String', + 'Tuple', + ] + + +class Saveable(): + """ + Base class for all saveable types + """ + default = None + ALL_SAVEABLES = {} + + @staticmethod + def to_json_compat(val): + """ + returns a json-compatible version of val + + should be overridden in saveable types that are not json compatible. + """ + return val + + @staticmethod + def to_python(val): + """ + convert from a json compatible version to the python version + + Must be overridden if not a one-to-one match + + This is where validation could be added as well. + """ + return val + + +class String(Saveable): + """ + A Saveable string + + Strings are the same in JSON as Python, so nothing to do here + """ + default = "" + + +class Bool(Saveable): + """ + A Saveable boolean + + Booleans are pretty much the same in JSON as Python, so nothing to do here + """ + default = False + + +class Int(Saveable): + + """ + A Saveable integer + + Integers are a little different in JSON than Python. Strictly speaking + JSON only has "numbers", which can be integer or float, so a little to + do here to make sure we get an int in Python. + """ + + default = 0 + + @staticmethod + def to_python(val): + """ + Convert a number to a python integer + """ + return int(val) + + +class Float(Saveable): + """ + A Saveable floating point number + + floats are a little different in JSON than Python. Strictly speaking + JSON only has "numbers", which can be integer or float, so a little to + do here to make sure we get a float in Python. + """ + + default = 0.0 + + @staticmethod + def to_python(val): + """ + Convert a number to a python float + """ + return float(val) + +# Container types: these need to hold Saveable objects. + + +class Tuple(Saveable): + """ + This assumes that whatever is in the tuple is Saveable or a "usual" + type: numbers, strings. + """ + default = () + + @staticmethod + def to_python(val): + """ + Convert a list to a tuple -- json only has one array type, + which matches to a list. + """ + # simply uses the List to_python method -- that part is the same. + return tuple(List.to_python(val)) + + +class List(Saveable): + """ + This assumes that whatever is in the list is Saveable or a "usual" + type: numbers, strings. + """ + default = [] + + @staticmethod + def to_json_compat(val): + lst = [] + for item in val: + try: + lst.append(item.to_json_compat()) + except AttributeError: + lst.append(item) + return lst + + @staticmethod + def to_python(val): + """ + Convert an array to a list. + + Complicated because list may contain non-json-compatible objects + """ + # try to reconstitute using the obj method + new_list = [] + for item in val: + try: + obj_type = item["__obj_type"] + obj = Saveable.ALL_SAVEABLES[obj_type].from_json_dict(item) + new_list.append(obj) + except (TypeError, KeyError): + new_list.append(item) + return new_list + + +class Dict(Saveable): + """ + This assumes that whatever in the dict is Saveable as well. + + This supports non-string keys, but all keys must be the same type. + """ + default = {} + + @staticmethod + def to_json_compat(val): + d = {} + # first key, arbitrarily + key_type = type(next(iter(val.keys()))) + if key_type is not str: + # need to add key_type to json + d['__key_not_string'] = True + key_not_string = True + else: + key_not_string = False + for key, item in val.items(): + kis = type(key) is str + if ((kis and key_not_string) or (not (kis or key_not_string))): + raise TypeError("dict keys must be all strings or no strings") + if key_type is not str: + # convert key to string + s_key = repr(key) + # make sure it can be reconstituted + if ast.literal_eval(s_key) != key: + raise ValueError(f"json save cannot save dicts with key:{key}") + else: + s_key = key + try: + d[s_key] = item.to_json_compat() + except AttributeError: + d[s_key] = item + return d + + @staticmethod + def to_python(val): + """ + Convert a json object to a dict + + Complicated because object may contain non-json-compatible objects + """ + + # try to reconstitute using the obj method + new_dict = {} + key_not_string = val.pop('__key_not_string', False) + for key, item in val.items(): + if key_not_string: + key = ast.literal_eval(key) + try: + obj_type = item["__obj_type"] + obj = Saveable.ALL_SAVEABLES[obj_type].from_json_dict(item) + new_dict[key] = obj + except (KeyError, TypeError): + new_dict[key] = item + return new_dict diff --git a/Student/rdrovdahl/lesson04/json_save/json_save/test/__init__.py b/Student/rdrovdahl/lesson04/json_save/json_save/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Student/rdrovdahl/lesson04/json_save/json_save/test/__pycache__/__init__.cpython-36.pyc b/Student/rdrovdahl/lesson04/json_save/json_save/test/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000..e82937a Binary files /dev/null and b/Student/rdrovdahl/lesson04/json_save/json_save/test/__pycache__/__init__.cpython-36.pyc differ diff --git a/Student/rdrovdahl/lesson04/json_save/json_save/test/__pycache__/test_json_save_dec.cpython-36-PYTEST.pyc b/Student/rdrovdahl/lesson04/json_save/json_save/test/__pycache__/test_json_save_dec.cpython-36-PYTEST.pyc new file mode 100644 index 0000000..52aa1d9 Binary files /dev/null and b/Student/rdrovdahl/lesson04/json_save/json_save/test/__pycache__/test_json_save_dec.cpython-36-PYTEST.pyc differ diff --git a/Student/rdrovdahl/lesson04/json_save/json_save/test/__pycache__/test_savables.cpython-36-PYTEST.pyc b/Student/rdrovdahl/lesson04/json_save/json_save/test/__pycache__/test_savables.cpython-36-PYTEST.pyc new file mode 100644 index 0000000..38929e5 Binary files /dev/null and b/Student/rdrovdahl/lesson04/json_save/json_save/test/__pycache__/test_savables.cpython-36-PYTEST.pyc differ diff --git a/Student/rdrovdahl/lesson04/json_save/json_save/test/temp.json b/Student/rdrovdahl/lesson04/json_save/json_save/test/temp.json new file mode 100644 index 0000000..760c652 --- /dev/null +++ b/Student/rdrovdahl/lesson04/json_save/json_save/test/temp.json @@ -0,0 +1,21 @@ +{ + "__obj_type": "ClassWithList", + "x": 34, + "lst": [ + { + "__obj_type": "SimpleClass", + "a": 3, + "b": 4.5 + }, + { + "__obj_type": "SimpleClass", + "a": 100, + "b": 5.2 + }, + { + "__obj_type": "SimpleClass", + "a": 34, + "b": 89.1 + } + ] +} \ No newline at end of file diff --git a/Student/rdrovdahl/lesson04/json_save/json_save/test/test_json_save_dec.py b/Student/rdrovdahl/lesson04/json_save/json_save/test/test_json_save_dec.py new file mode 100644 index 0000000..f587517 --- /dev/null +++ b/Student/rdrovdahl/lesson04/json_save/json_save/test/test_json_save_dec.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python + +""" +test code for the decorator version of json_save +""" + +import pytest + +import json_save.json_save_dec as js + + +# Some simple classes to test: + +@js.json_save +class NoInit: + """ + A class with saveable attribute, but no __init__ + """ + x = js.Int() + y = js.String() + + +@js.json_save +class SimpleClass: + + a = js.Int() + b = js.Float() + + def __init__(self, a=None, b=None): + if a is not None: + self.a = a + if b is not None: + self.b = b + + +@js.json_save +class ClassWithList: + + x = js.Int() + lst = js.List() + + def __init__(self, x, lst): + self.x = x + self.lst = lst + + +@js.json_save +class ClassWithDict: + x = js.Int() + d = js.Dict() + + def __init__(self, x, d): + self.x = x + self.d = d + + +@pytest.fixture +def nested_example(): + l = [SimpleClass(3, 4.5), + SimpleClass(100, 5.2), + SimpleClass(34, 89.1), + ] + + return ClassWithList(34, l) + +@pytest.fixture +def nested_dict(): + d = {'this': SimpleClass(3, 4.5), + 'that': SimpleClass(100, 5.2), + 'other': SimpleClass(34, 89.1), + } + + return ClassWithDict(34, d) + + +# now the actual test code + +def test_hasattr(): + """ + checks that the default attributes get set if they are not created by an __init__ + """ + ts = NoInit() + # has the instance attributes even though no __init__ exists + # they should be the default values + assert ts.x == 0 + assert ts.y == "" + + +def test_attrs(): + ts = SimpleClass() + + attrs = ts._attrs_to_save + assert list(attrs.keys()) == ['a', 'b'] + + +def test_simple_save(): + + ts = SimpleClass() + ts.a = 5 + ts.b = 3.14 + + saved = ts.to_json_compat() + assert saved['a'] == 5 + assert saved['b'] == 3.14 + assert saved['__obj_type'] == 'SimpleClass' + + +def test_list_attr(): + + cwl = ClassWithList(10, [1, 5, 2, 8]) + + saved = cwl.to_json_compat() + assert saved['x'] == 10 + assert saved['lst'] == [1, 5, 2, 8] + assert saved['__obj_type'] == 'ClassWithList' + + +def test_nested(nested_example): + + saved = nested_example.to_json_compat() + + assert saved['x'] == 34 + assert len(saved['lst']) == 3 + for obj in saved['lst']: + assert obj['__obj_type'] == 'SimpleClass' + + +def test_save_load_simple(): + sc = SimpleClass(5, 3.14) + + jc = sc.to_json_compat() + + # re-create it from the dict: + sc2 = SimpleClass.from_json_dict(jc) + + assert sc == sc2 + + +def test_save_load_nested(nested_example): + + jc = nested_example.to_json_compat() + + # re-create it from the dict: + nested_example2 = ClassWithList.from_json_dict(jc) + + assert nested_example == nested_example2 + + +def test_from_json_dict(nested_example): + + j_dict = nested_example.to_json_compat() + + reconstructed = js.from_json_dict(j_dict) + + assert reconstructed == nested_example + + +def test_from_json(nested_example): + """ + can it be re-created from an actual json string? + """ + + json_str = nested_example.to_json() + + reconstructed = js.from_json(json_str) + + assert reconstructed == nested_example + + +def test_from_json_file(nested_example): + """ + can it be re-created from an actual json file? + """ + + json_str = nested_example.to_json() + with open("temp.json", 'w') as tempfile: + tempfile.write(nested_example.to_json()) + + with open("temp.json") as tempfile: + reconstructed = js.from_json(tempfile) + + reconstructed = js.from_json(json_str) + + assert reconstructed == nested_example + + +def test_dict(): + """ + a simple class with a dict attribute + """ + cwd = ClassWithDict(45, {"this": 34, "that": 12}) + + # see if it can be reconstructed + + jc = cwd.to_json_compat() + + # re-create it from the dict: + cwd2 = ClassWithDict.from_json_dict(jc) + + assert cwd == cwd2 + + +def test_from_json_dict2(nested_dict): + """ + can it be re-created from an actual json string? + """ + + json_str = nested_dict.to_json() + reconstructed = js.from_json(json_str) + + assert reconstructed == nested_dict + + +def test_eq(): + sc1 = SimpleClass(3, 4.5) + sc2 = SimpleClass(3, 4.5) + + assert sc1 == sc2 + + +def test_not_eq(): + sc1 = SimpleClass(3, 4.5) + sc2 = SimpleClass(3, 4.4) + + assert sc1 != sc2 + + +def test_not_eq_reconstruct(): + sc1 = SimpleClass.from_json_dict(SimpleClass(3, 4.5).to_json_compat()) + sc2 = SimpleClass.from_json_dict(SimpleClass(2, 4.5).to_json_compat()) + + assert sc1 != sc2 + assert sc2 != sc1 + + +def test_not_valid(): + """ + You should get an error trying to make a savable class with + no savable attributes. + """ + with pytest.raises(TypeError): + @js.json_save + class NotValid(): + pass + + +def test_not_valid_class_not_instance(): + """ + You should get an error trying to make a savable class with + no savable attributes. + """ + with pytest.raises(TypeError): + @js.json_save + class NotValid(): + a = js.Int + b = js.Float diff --git a/Student/rdrovdahl/lesson04/json_save/json_save/test/test_json_save_meta.py b/Student/rdrovdahl/lesson04/json_save/json_save/test/test_json_save_meta.py new file mode 100644 index 0000000..2f42c22 --- /dev/null +++ b/Student/rdrovdahl/lesson04/json_save/json_save/test/test_json_save_meta.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 + +""" +tests for json_save +""" + +import json_save.json_save_meta as js + +import pytest + + +class NoInit(js.JsonSaveable): + x = js.Int() + y = js.String() + + +# A few simple examples to test +class SimpleClass(js.JsonSaveable): + + a = js.Int() + b = js.Float() + + def __init__(self, a=None, b=None): + if a is not None: + self.a = a + if b is not None: + self.b = b + + +class ClassWithList(js.JsonSaveable): + + x = js.Int() + lst = js.List() + + def __init__(self, x, lst): + self.x = x + self.lst = lst + + +class ClassWithDict(js.JsonSaveable): + x = js.Int() + d = js.Dict() + + def __init__(self, x, d): + self.x = x + self.d = d + + +@pytest.fixture +def nested_example(): + lst = [SimpleClass(3, 4.5), + SimpleClass(100, 5.2), + SimpleClass(34, 89.1), + ] + + return ClassWithList(34, lst) + + +@pytest.fixture +def nested_dict(): + d = {'this': SimpleClass(3, 4.5), + 'that': SimpleClass(100, 5.2), + 'other': SimpleClass(34, 89.1), + } + + return ClassWithDict(34, d) + + +def test_hasattr(): + ts = NoInit() + # has the attributes even though no __init__ exists + # they should be the default values + assert ts.x == 0 + assert ts.y == "" + + +def test_simple_save(): + + ts = SimpleClass() + ts.a = 5 + ts.b = 3.14 + + saved = ts.to_json_compat() + assert saved['a'] == 5 + assert saved['b'] == 3.14 + assert saved['__obj_type'] == 'SimpleClass' + + +def test_list_attr(): + + cwl = ClassWithList(10, [1, 5, 2, 8]) + + saved = cwl.to_json_compat() + assert saved['x'] == 10 + assert saved['lst'] == [1, 5, 2, 8] + assert saved['__obj_type'] == 'ClassWithList' + + +def test_nested(nested_example): + + saved = nested_example.to_json_compat() + + assert saved['x'] == 34 + assert len(saved['lst']) == 3 + for obj in saved['lst']: + assert obj['__obj_type'] == 'SimpleClass' + + +def test_save_load_simple(): + sc = SimpleClass(5, 3.14) + + jc = sc.to_json_compat() + + # re-create it from the dict: + sc2 = SimpleClass.from_json_dict(jc) + + assert sc == sc2 + + +def test_save_load_nested(nested_example): + + jc = nested_example.to_json_compat() + + # re-create it from the dict: + nested_example2 = ClassWithList.from_json_dict(jc) + + assert nested_example == nested_example2 + + +def test_from_json_dict(nested_example): + + j_dict = nested_example.to_json_compat() + + reconstructed = js.from_json_dict(j_dict) + + assert reconstructed == nested_example + + +def test_from_json(nested_example): + """ + can it be re-created from an actual json string? + """ + + json_str = nested_example.to_json() + + reconstructed = js.from_json(json_str) + + assert reconstructed == nested_example + + +def test_from_json_file(nested_example): + """ + can it be re-created from an actual json file? + """ + + json_str = nested_example.to_json() + with open("temp.json", 'w') as tempfile: + tempfile.write(nested_example.to_json()) + + with open("temp.json") as tempfile: + reconstructed = js.from_json(tempfile) + + reconstructed = js.from_json(json_str) + + assert reconstructed == nested_example + + +def test_dict(): + """ + a simple class with a dict attribute + """ + cwd = ClassWithDict(45, {"this": 34, "that": 12}) + + # see if it can be reconstructed + + jc = cwd.to_json_compat() + + # re-create it from the dict: + cwd2 = ClassWithDict.from_json_dict(jc) + + assert cwd == cwd2 + + +def test_from_json_dict2(nested_dict): + """ + can it be re-created from an actual json string? + """ + + json_str = nested_dict.to_json() + + reconstructed = js.from_json(json_str) + + assert reconstructed == nested_dict + +def test_eq(): + sc1 = SimpleClass(3, 4.5) + sc2 = SimpleClass(3, 4.5) + + assert sc1 == sc2 + + +def test_not_eq(): + sc1 = SimpleClass(3, 4.5) + sc2 = SimpleClass(3, 4.4) + + assert sc1 != sc2 + + +def test_not_eq_reconstruct(): + sc1 = SimpleClass.from_json_dict(SimpleClass(3, 4.5).to_json_compat()) + sc2 = SimpleClass.from_json_dict(SimpleClass(2, 4.5).to_json_compat()) + + assert sc1 != sc2 + assert sc2 != sc1 + + +def test_not_valid(): + """ + You should get an error trying to make a savable class with + no savable attributes. + """ + with pytest.raises(TypeError): + class NotValid(js.JsonSaveable): + pass + + +def test_not_valid_class_not_instance(): + """ + You should get an error trying to make a savable class with + no savable attributes. + """ + with pytest.raises(TypeError): + class NotValid(js.JsonSaveable): + a = js.Int + b = js.Float diff --git a/Student/rdrovdahl/lesson04/json_save/json_save/test/test_savables.py b/Student/rdrovdahl/lesson04/json_save/json_save/test/test_savables.py new file mode 100644 index 0000000..831374c --- /dev/null +++ b/Student/rdrovdahl/lesson04/json_save/json_save/test/test_savables.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python + +""" +tests for the savable objects +""" +import pytest + +import json + +from json_save.saveables import * + +# The simple, almost json <-> python ones: +# Type, default, example +basics = [(String, "This is a string"), + (Int, 23), + (Float, 3.1458), + (Bool, True), + (Bool, False), + (List, [2, 3, 4]), + (Tuple, (1, 2, 3.4, "this")), + (List, [[1, 2, 3], [4, 5, 6]]), + (List, [{"3": 34}, {"4": 5}]), # list with dicts in it. + (Dict, {"this": {"3": 34}, "that": {"4": 5}}) # dict with dicts + ] + + +@pytest.mark.parametrize(('Type', 'val'), basics) +def test_basics(Type, val): + js = json.dumps(Type.to_json_compat(val)) + val2 = Type.to_python(json.loads(js)) + assert val == val2 + assert type(val) == type(val2) + + +nested = [(List, [(1, 2), (3, 4), (5, 6)]), # tuple in list + (Tuple, ((1, 2), (3, 4), (5, 6))), # tuple in tuple + ] + + +# This maybe should be fixed in the future?? +@pytest.mark.xfail(reason="nested not-standard types not supported") +@pytest.mark.parametrize(('Type', 'val'), nested) +def test_nested(Type, val): + print("original value:", val) + js = json.dumps(Type.to_json_compat(val)) + print("js is:", js) + val2 = Type.to_python(json.loads(js)) + print("new value is:", val2) + assert val == val2 + assert type(val) == type(val2) + + + + +dicts = [{"this": 14, "that": 1.23}, + {34: 15, 23: 5}, + {3.4: "float_key", 1.2: "float_key"}, + {(1, 2, 3): "tuple_key"}, + {(3, 4, 5): "tuple_int", ("this", "that"): "tuple_str"}, + {4: "int_key", 1.23: "float_key", (1, 2, 3): "tuple_key"}, + ] + + +@pytest.mark.parametrize('val', dicts) +def test_dicts(val): + js = json.dumps(Dict.to_json_compat(val)) + val2 = Dict.to_python(json.loads(js)) + assert val == val2 + assert type(val) == type(val2) + # check that the types of the keys is the same + for k1, k2 in zip(val.keys(), val2.keys()): + assert type(k1) is type(k2) + + +# These are dicts that can't be saved +# -- mixing string and non-string keys +bad_dicts = [{"this": "string_key", 4: "int_key"}, + {3: "int_key", "this": "string_key"}, + {None: "none_key", "this": "string_key"}, + {"this": "string_key", None: "none_key"}, + ] + + +@pytest.mark.parametrize("val", bad_dicts) +def test_bad_dicts(val): + with pytest.raises(TypeError): + Dict.to_json_compat(val) diff --git a/Student/rdrovdahl/lesson04/json_save/setup.py b/Student/rdrovdahl/lesson04/json_save/setup.py new file mode 100755 index 0000000..d85dd95 --- /dev/null +++ b/Student/rdrovdahl/lesson04/json_save/setup.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python + +""" +This is about as simple a setup.py as you can have + +But its enough to support the json_save package + +""" + +import os + +from setuptools import setup, find_packages + + +def get_version(): + """ + Reads the version string from the package __init__ and returns it + """ + with open(os.path.join("json_save", "__init__.py")) as init_file: + for line in init_file: + parts = line.strip().partition("=") + if parts[0].strip() == "__version__": + return parts[2].strip().strip("'").strip('"') + return None + + +setup( + name='json_save', + version=get_version(), + author='Chris Barker', + author_email='PythonCHB@gmail.com', + packages=find_packages(), + # license='LICENSE.txt', + description='Metaclass based system for saving object to JSON', + long_description=open('README.txt').read(), +) diff --git a/Student/rdrovdahl/lesson04/mailroom/.cache/v/cache/lastfailed b/Student/rdrovdahl/lesson04/mailroom/.cache/v/cache/lastfailed new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/Student/rdrovdahl/lesson04/mailroom/.cache/v/cache/lastfailed @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/Student/rdrovdahl/lesson04/mailroom/__pycache__/mailroom_OO.cpython-36.pyc b/Student/rdrovdahl/lesson04/mailroom/__pycache__/mailroom_OO.cpython-36.pyc new file mode 100644 index 0000000..35d69dc Binary files /dev/null and b/Student/rdrovdahl/lesson04/mailroom/__pycache__/mailroom_OO.cpython-36.pyc differ diff --git a/Student/rdrovdahl/lesson04/mailroom/__pycache__/test_mailroom_OO.cpython-36-PYTEST.pyc b/Student/rdrovdahl/lesson04/mailroom/__pycache__/test_mailroom_OO.cpython-36-PYTEST.pyc new file mode 100644 index 0000000..3fbec30 Binary files /dev/null and b/Student/rdrovdahl/lesson04/mailroom/__pycache__/test_mailroom_OO.cpython-36-PYTEST.pyc differ diff --git a/Student/rdrovdahl/lesson04/mailroom/json_save/.cache/v/cache/lastfailed b/Student/rdrovdahl/lesson04/mailroom/json_save/.cache/v/cache/lastfailed new file mode 100644 index 0000000..051b538 --- /dev/null +++ b/Student/rdrovdahl/lesson04/mailroom/json_save/.cache/v/cache/lastfailed @@ -0,0 +1,12 @@ +{ + "json_save/test/test_savables.py::test_bad_dicts[val4]": true, + "json_save/test/test_savables.py::test_basics": true, + "json_save/test/test_savables.py::test_basics[Tuple-val5]": true, + "json_save/test/test_savables.py::test_containers[Dict-val0]": true, + "json_save/test/test_savables.py::test_dicts[Dict-val0]": true, + "json_save/test/test_savables.py::test_dicts[Dict-val1]": true, + "json_save/test/test_savables.py::test_dicts[Dict-val2]": true, + "json_save/test/test_savables.py::test_dicts[Dict-val3]": true, + "json_save/test/test_savables.py::test_dicts[val10]": true, + "json_save/test/test_savables.py::test_dicts[val7]": true +} \ No newline at end of file diff --git a/Student/rdrovdahl/lesson04/mailroom/json_save/README.txt b/Student/rdrovdahl/lesson04/mailroom/json_save/README.txt new file mode 100644 index 0000000..a80b252 --- /dev/null +++ b/Student/rdrovdahl/lesson04/mailroom/json_save/README.txt @@ -0,0 +1,4 @@ +This is a simple meta-class based system for saving objects in the JSON format. + +It can make any arbitrary class savable and re-loadable from JSON. + diff --git a/Student/rdrovdahl/lesson04/mailroom/json_save/examples/example_dec.py b/Student/rdrovdahl/lesson04/mailroom/json_save/examples/example_dec.py new file mode 100644 index 0000000..2e1a224 --- /dev/null +++ b/Student/rdrovdahl/lesson04/mailroom/json_save/examples/example_dec.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python + +""" +Examples of using json_save +""" + +import json_save.json_save_dec as js + + +# Examples using the decorator + +@js.json_save +class MyClass: + + x = js.Int() + y = js.Float() + lst = js.List() + + def __init__(self, x, lst): + self.x = x + self.lst = lst + + +@js.json_save +class OtherSaveable: + + foo = js.String() + bar = js.Int() + + def __init__(self, foo, bar): + self.foo = foo + self.bar = bar + + +# create one: +print("about to create a instance") +mc = MyClass(5, [3, 5, 7, 9]) + +print(mc) + +jc = mc.to_json_compat() + +# re-create it from the dict: +mc2 = MyClass.from_json_dict(jc) + +print(mc2 == "fred") + +assert mc2 == mc + +print(mc.to_json()) + +# now try it nested... +mc_nest = MyClass(34, [OtherSaveable("this", 2), + OtherSaveable("that", 64), + ]) + +mc_nest_comp = mc_nest.to_json_compat() +print(mc_nest_comp) + +# can we re-create it? +mc_nest2 = MyClass.from_json_dict(mc_nest_comp) + +print(mc_nest) +print(mc_nest2) + +assert mc_nest == mc_nest2 + diff --git a/Student/rdrovdahl/lesson04/mailroom/json_save/examples/example_meta.py b/Student/rdrovdahl/lesson04/mailroom/json_save/examples/example_meta.py new file mode 100644 index 0000000..13719ab --- /dev/null +++ b/Student/rdrovdahl/lesson04/mailroom/json_save/examples/example_meta.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python + +""" +Examples of using json_save +""" + +import json_save.json_save_meta as js + +# Metaclass examples + +class MyClass(js.JsonSaveable): + + x = js.Int() + y = js.Float() + lst = js.List() + + def __init__(self, x, lst): + self.x = x + self.lst = lst + + +class OtherSaveable(js.JsonSaveable): + + foo = js.String() + bar = js.Int() + + def __init__(self, foo, bar): + self.foo = foo + self.bar = bar + +# create one: +print("about to create a instance") +mc = MyClass(5, [3, 5, 7, 9]) + +print(mc) + +jc = mc.to_json_compat() + +# re-create it from the dict: +mc2 = MyClass.from_json_dict(jc) + +print(mc2 == "fred") + +assert mc2 == mc + +print(mc.to_json()) + +# now try it nested... +mc_nest = MyClass(34, [OtherSaveable("this", 2), + OtherSaveable("that", 64), + ]) + +mc_nest_comp = mc_nest.to_json_compat() +print(mc_nest_comp) + +# can we re-create it? +mc_nest2 = MyClass.from_json_dict(mc_nest_comp) + +print(mc_nest) +print(mc_nest2) + +assert mc_nest == mc_nest2 + diff --git a/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/__init__.py b/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/__init__.py new file mode 100644 index 0000000..db59bd9 --- /dev/null +++ b/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/__init__.py @@ -0,0 +1,8 @@ +""" +json_save package + +Pulling in names from the other packages. +""" + +__version__ = "0.4.0" + diff --git a/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/__pycache__/__init__.cpython-36.pyc b/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000..e239790 Binary files /dev/null and b/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/__pycache__/__init__.cpython-36.pyc differ diff --git a/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/__pycache__/json_save_dec.cpython-36.pyc b/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/__pycache__/json_save_dec.cpython-36.pyc new file mode 100644 index 0000000..5db4929 Binary files /dev/null and b/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/__pycache__/json_save_dec.cpython-36.pyc differ diff --git a/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/__pycache__/json_save_meta.cpython-36.pyc b/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/__pycache__/json_save_meta.cpython-36.pyc new file mode 100644 index 0000000..af67c04 Binary files /dev/null and b/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/__pycache__/json_save_meta.cpython-36.pyc differ diff --git a/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/__pycache__/saveables.cpython-36.pyc b/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/__pycache__/saveables.cpython-36.pyc new file mode 100644 index 0000000..85007ae Binary files /dev/null and b/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/__pycache__/saveables.cpython-36.pyc differ diff --git a/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/json_save_dec.py b/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/json_save_dec.py new file mode 100644 index 0000000..5af4de9 --- /dev/null +++ b/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/json_save_dec.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python + +""" +json_save implemented as a decorator +""" + +import json +from pathlib import Path + +from .json_save_meta import * + + +# assorted methods that will need to be added to the decorated class: +def _to_json_compat(self): + """ + converts this object to a json-compatible dict. + + returns the dict + """ + # add and __obj_type attribute, so it can be reconstructed + dic = {"__obj_type": self.__class__.__qualname__} + for attr, typ in self._attrs_to_save.items(): + dic[attr] = typ.to_json_compat(getattr(self, attr)) + return dic + + +def __eq__(self, other): + """ + default equality method that checks if all of the saved attributes + are equal + """ + for attr in self._attrs_to_save: + try: + if getattr(self, attr) != getattr(other, attr): + return False + except AttributeError: + return False + return True + +@classmethod +def _from_json_dict(cls, dic): + """ + creates an instance of this class populated by the contents of + the json compatible dict + + the object is created with __new__ before setting the attributes + + NOTE: __init__ is not called. + There should not be any extra initialization required in __init__ + """ + # create a new object + obj = cls.__new__(cls) + for attr, typ in cls._attrs_to_save.items(): + setattr(obj, attr, typ.to_python(dic[attr])) + return obj + + +def __new__(cls, *args, **kwargs): + """ + This adds instance attributes to assure they are all there, even if + they are not set in the subclasses __init__ + + it's in __new__ so that it will get called before the decorated class' + __init__ -- the __init__ will override anything here. + """ + # create the instance by calling the base class __new__ + obj = cls.__base__.__new__(cls) + # using super() did not work here -- why?? + # set the instance attributes to defaults + for attr, typ in cls._attrs_to_save.items(): + setattr(obj, attr, typ.default) + return obj + + +def _to_json(self, fp=None, indent=4): + """ + Converts the object to JSON + + :param fp=None: an open file_like object to write the json to. + If it is None, then a string with the JSON + will be returned as a string + + :param indent=4: The indentation level desired in the JSON + """ + if fp is None: + return json.dumps(self.to_json_compat(), indent=indent) + else: + json.dump(self.to_json_compat(), fp, indent=indent) + + +# now the actual decorator +def json_save(cls): + """ + json_save decorator + + makes decorated classes Saveable to json + """ + # make sure this is decorating a class object + if type(cls) is not type: + raise TypeError("json_save can only be used on classes") + + # find the saveable attributes + # these will the attributes that get saved and reconstructed from json. + # each class object gets its own dict + attr_dict = vars(cls) + cls._attrs_to_save = {} + for key, attr in attr_dict.items(): + if isinstance(attr, Saveable): + cls._attrs_to_save[key] = attr + if not cls._attrs_to_save: + raise TypeError(f"{cls.__name__} class has no saveable attributes.\n" + " Note that Savable attributes must be instances") + # register this class so we can re-construct instances. + Saveable.ALL_SAVEABLES[cls.__qualname__] = cls + + # add the methods: + cls.__new__ = __new__ + cls.to_json_compat = _to_json_compat + cls.__eq__ = __eq__ + cls.from_json_dict = _from_json_dict + cls.to_json = _to_json + + return cls + + +# utilities for loading arbitrary objects from json +def from_json_dict(j_dict): + """ + factory function that creates an arbitrary JsonSaveable + object from a json-compatible dict. + """ + # determine the class it is. + obj_type = j_dict["__obj_type"] + obj = Saveable.ALL_SAVEABLES[obj_type].from_json_dict(j_dict) + return obj + + +def from_json(_json): + """ + Factory function that re-creates a JsonSaveable object + from a json string or file + """ + if isinstance(_json, (str, Path)): + return from_json_dict(json.loads(_json)) + else: # assume a file-like object + return from_json_dict(json.load(_json)) diff --git a/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/json_save_meta.py b/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/json_save_meta.py new file mode 100644 index 0000000..5b8dfa8 --- /dev/null +++ b/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/json_save_meta.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 + +""" +json_save + +metaclass based system for saving objects in a JSON format + +This could be useful, but it's kept simple to show the use of metaclasses + +The idea is that you subclass from JsonSavable, and then you get an object +that be saved and reloaded to/from JSON +""" + +import json + +# import * is a bad idea in general, but helpful for a modules that's part +# of a package, where you control the names. +from .saveables import * + + +class MetaJsonSaveable(type): + """ + The metaclass for creating JsonSavable classes + + Deriving from type makes it a metaclass. + + Note: the __init__ gets run at compile time, not run time. + (module import time) + """ + def __init__(cls, name, bases, attr_dict): + # it gets the class object as the first param. + # and then the same parameters as the type() factory function + + # you want to call the regular type initilizer: + super().__init__(name, bases, attr_dict) + + # here's where we work with the class attributes: + # these will the attributes that get saved and reconstructed from json. + # each class object gets its own dict + cls._attrs_to_save = {} + for key, attr in attr_dict.items(): + if isinstance(attr, Saveable): + cls._attrs_to_save[key] = attr + # special case JsonSaveable -- no attrs to save yet + if cls.__name__ != "JsonSaveable" and (not cls._attrs_to_save): + raise TypeError(f"{cls.__name__} class has no saveable attributes.\n" + " Note that Savable attributes must be instances") + + # register this class so we can re-construct instances. + Saveable.ALL_SAVEABLES[attr_dict["__qualname__"]] = cls + + +class JsonSaveable(metaclass=MetaJsonSaveable): + """ + mixin for JsonSavable objects + """ + def __new__(cls, *args, **kwargs): + """ + This adds instance attributes to assure they are all there, even if + they are not set in the subclasses __init__ + """ + # create the instance + obj = super().__new__(cls) + # set the instance attributes to defaults + for attr, typ in cls._attrs_to_save.items(): + setattr(obj, attr, typ.default) + return obj + + def __eq__(self, other): + """ + default equality method that checks if all of the saved attributes + are equal + """ + for attr in self._attrs_to_save: + try: + if getattr(self, attr) != getattr(other, attr): + return False + except AttributeError: + return False + return True + + def to_json_compat(self): + """ + converts this object to a json-compatible dict. + + returns the dict + """ + # add and __obj_type attribute, so it can be reconstructed + dic = {"__obj_type": self.__class__.__qualname__} + for attr, typ in self._attrs_to_save.items(): + dic[attr] = typ.to_json_compat(getattr(self, attr)) + return dic + + @classmethod + def from_json_dict(cls, dic): + """ + creates an instance of this class populated by the contents of + the json compatible dict + + the object is created with __new__ before setting the attributes + + NOTE: __init__ is not called. + There should not be any extra initialization required in __init__ + """ + # create a new object + obj = cls.__new__(cls) + for attr, typ in cls._attrs_to_save.items(): + setattr(obj, attr, typ.to_python(dic[attr])) + # make sure it gets initialized + # obj.__init__() + return obj + + def to_json(self, fp=None, indent=4): + """ + Converts the object to JSON + + :param fp=None: an open file_like object to write the json to. + If it is None, then a string with the JSON + will be returned as a string + + :param indent=4: The indentation level desired in the JSON + """ + if fp is None: + return json.dumps(self.to_json_compat(), indent=indent) + else: + json.dump(self.to_json_compat(), fp, indent=indent) + + def __str__(self): + msg = ["{} object, with attributes:".format(self.__class__.__qualname__)] + for attr in self._attrs_to_save.keys(): + msg.append("{}: {}".format(attr, getattr(self, attr))) + return "\n".join(msg) + + +def from_json_dict(j_dict): + """ + factory function that creates an arbitrary JsonSavable + object from a json-compatible dict. + """ + # determine the class it is. + obj_type = j_dict["__obj_type"] + obj = Saveable.ALL_SAVEABLES[obj_type].from_json_dict(j_dict) + return obj + + +def from_json(_json): + """ + factory function that re-creates a JsonSavable object + from a json string or file + """ + if isinstance(_json, str): + return from_json_dict(json.loads(_json)) + else: # assume a file-like object + return from_json_dict(json.load(_json)) + + +if __name__ == "__main__": + + # Example of using it. + class MyClass(JsonSaveable): + + x = Int() + y = Float() + l = List() + + def __init__(self, x, lst): + self.x = x + self.lst = lst + + + class OtherSaveable(JsonSavable): + + foo = String() + bar = Int() + + def __init__(self, foo, bar): + self.foo = foo + self.bar = bar + + # create one: + print("about to create a instance") + mc = MyClass(5, [3, 5, 7, 9]) + + print(mc) + + jc = mc.to_json_compat() + + # re-create it from the dict: + mc2 = MyClass.from_json_dict(jc) + + print(mc2 == "fred") + + assert mc2 == mc + + print(mc.to_json()) + + # now try it nested... + mc_nest = MyClass(34, [OtherSaveable("this", 2), + OtherSaveable("that", 64), + ]) + + mc_nest_comp = mc_nest.to_json_compat() + print(mc_nest_comp) + + # can we re-create it? + mc_nest2 = MyClass.from_json_dict(mc_nest_comp) + + print(mc_nest) + print(mc_nest2) + + assert mc_nest == mc_nest2 diff --git a/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/saveables.py b/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/saveables.py new file mode 100644 index 0000000..16ac75f --- /dev/null +++ b/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/saveables.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python + +""" +The Saveable objects used by both the metaclass and decorator approach. +""" +import ast + +# import json + +__all__ = ['Bool', + 'Dict', + 'Float', + 'Int', + 'List', + 'Saveable', + 'String', + 'Tuple', + ] + + +class Saveable(): + """ + Base class for all saveable types + """ + default = None + ALL_SAVEABLES = {} + + @staticmethod + def to_json_compat(val): + """ + returns a json-compatible version of val + + should be overridden in saveable types that are not json compatible. + """ + return val + + @staticmethod + def to_python(val): + """ + convert from a json compatible version to the python version + + Must be overridden if not a one-to-one match + + This is where validation could be added as well. + """ + return val + + +class String(Saveable): + """ + A Saveable string + + Strings are the same in JSON as Python, so nothing to do here + """ + default = "" + + +class Bool(Saveable): + """ + A Saveable boolean + + Booleans are pretty much the same in JSON as Python, so nothing to do here + """ + default = False + + +class Int(Saveable): + + """ + A Saveable integer + + Integers are a little different in JSON than Python. Strictly speaking + JSON only has "numbers", which can be integer or float, so a little to + do here to make sure we get an int in Python. + """ + + default = 0 + + @staticmethod + def to_python(val): + """ + Convert a number to a python integer + """ + return int(val) + + +class Float(Saveable): + """ + A Saveable floating point number + + floats are a little different in JSON than Python. Strictly speaking + JSON only has "numbers", which can be integer or float, so a little to + do here to make sure we get a float in Python. + """ + + default = 0.0 + + @staticmethod + def to_python(val): + """ + Convert a number to a python float + """ + return float(val) + +# Container types: these need to hold Saveable objects. + + +class Tuple(Saveable): + """ + This assumes that whatever is in the tuple is Saveable or a "usual" + type: numbers, strings. + """ + default = () + + @staticmethod + def to_python(val): + """ + Convert a list to a tuple -- json only has one array type, + which matches to a list. + """ + # simply uses the List to_python method -- that part is the same. + return tuple(List.to_python(val)) + + +class List(Saveable): + """ + This assumes that whatever is in the list is Saveable or a "usual" + type: numbers, strings. + """ + default = [] + + @staticmethod + def to_json_compat(val): + lst = [] + for item in val: + try: + lst.append(item.to_json_compat()) + except AttributeError: + lst.append(item) + return lst + + @staticmethod + def to_python(val): + """ + Convert an array to a list. + + Complicated because list may contain non-json-compatible objects + """ + # try to reconstitute using the obj method + new_list = [] + for item in val: + try: + obj_type = item["__obj_type"] + obj = Saveable.ALL_SAVEABLES[obj_type].from_json_dict(item) + new_list.append(obj) + except (TypeError, KeyError): + new_list.append(item) + return new_list + + +class Dict(Saveable): + """ + This assumes that whatever in the dict is Saveable as well. + + This supports non-string keys, but all keys must be the same type. + """ + default = {} + + @staticmethod + def to_json_compat(val): + d = {} + # first key, arbitrarily + key_type = type(next(iter(val.keys()))) + if key_type is not str: + # need to add key_type to json + d['__key_not_string'] = True + key_not_string = True + else: + key_not_string = False + for key, item in val.items(): + kis = type(key) is str + if ((kis and key_not_string) or (not (kis or key_not_string))): + raise TypeError("dict keys must be all strings or no strings") + if key_type is not str: + # convert key to string + s_key = repr(key) + # make sure it can be reconstituted + if ast.literal_eval(s_key) != key: + raise ValueError(f"json save cannot save dicts with key:{key}") + else: + s_key = key + try: + d[s_key] = item.to_json_compat() + except AttributeError: + d[s_key] = item + return d + + @staticmethod + def to_python(val): + """ + Convert a json object to a dict + + Complicated because object may contain non-json-compatible objects + """ + + # try to reconstitute using the obj method + new_dict = {} + key_not_string = val.pop('__key_not_string', False) + for key, item in val.items(): + if key_not_string: + key = ast.literal_eval(key) + try: + obj_type = item["__obj_type"] + obj = Saveable.ALL_SAVEABLES[obj_type].from_json_dict(item) + new_dict[key] = obj + except (KeyError, TypeError): + new_dict[key] = item + return new_dict diff --git a/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/test/__init__.py b/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/test/__pycache__/__init__.cpython-36.pyc b/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/test/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000..c64ac9e Binary files /dev/null and b/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/test/__pycache__/__init__.cpython-36.pyc differ diff --git a/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/test/__pycache__/test_json_save_dec.cpython-36-PYTEST.pyc b/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/test/__pycache__/test_json_save_dec.cpython-36-PYTEST.pyc new file mode 100644 index 0000000..3ed0dcd Binary files /dev/null and b/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/test/__pycache__/test_json_save_dec.cpython-36-PYTEST.pyc differ diff --git a/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/test/__pycache__/test_savables.cpython-36-PYTEST.pyc b/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/test/__pycache__/test_savables.cpython-36-PYTEST.pyc new file mode 100644 index 0000000..38929e5 Binary files /dev/null and b/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/test/__pycache__/test_savables.cpython-36-PYTEST.pyc differ diff --git a/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/test/temp.json b/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/test/temp.json new file mode 100644 index 0000000..760c652 --- /dev/null +++ b/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/test/temp.json @@ -0,0 +1,21 @@ +{ + "__obj_type": "ClassWithList", + "x": 34, + "lst": [ + { + "__obj_type": "SimpleClass", + "a": 3, + "b": 4.5 + }, + { + "__obj_type": "SimpleClass", + "a": 100, + "b": 5.2 + }, + { + "__obj_type": "SimpleClass", + "a": 34, + "b": 89.1 + } + ] +} \ No newline at end of file diff --git a/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/test/test_json_save_dec.py b/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/test/test_json_save_dec.py new file mode 100644 index 0000000..f587517 --- /dev/null +++ b/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/test/test_json_save_dec.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python + +""" +test code for the decorator version of json_save +""" + +import pytest + +import json_save.json_save_dec as js + + +# Some simple classes to test: + +@js.json_save +class NoInit: + """ + A class with saveable attribute, but no __init__ + """ + x = js.Int() + y = js.String() + + +@js.json_save +class SimpleClass: + + a = js.Int() + b = js.Float() + + def __init__(self, a=None, b=None): + if a is not None: + self.a = a + if b is not None: + self.b = b + + +@js.json_save +class ClassWithList: + + x = js.Int() + lst = js.List() + + def __init__(self, x, lst): + self.x = x + self.lst = lst + + +@js.json_save +class ClassWithDict: + x = js.Int() + d = js.Dict() + + def __init__(self, x, d): + self.x = x + self.d = d + + +@pytest.fixture +def nested_example(): + l = [SimpleClass(3, 4.5), + SimpleClass(100, 5.2), + SimpleClass(34, 89.1), + ] + + return ClassWithList(34, l) + +@pytest.fixture +def nested_dict(): + d = {'this': SimpleClass(3, 4.5), + 'that': SimpleClass(100, 5.2), + 'other': SimpleClass(34, 89.1), + } + + return ClassWithDict(34, d) + + +# now the actual test code + +def test_hasattr(): + """ + checks that the default attributes get set if they are not created by an __init__ + """ + ts = NoInit() + # has the instance attributes even though no __init__ exists + # they should be the default values + assert ts.x == 0 + assert ts.y == "" + + +def test_attrs(): + ts = SimpleClass() + + attrs = ts._attrs_to_save + assert list(attrs.keys()) == ['a', 'b'] + + +def test_simple_save(): + + ts = SimpleClass() + ts.a = 5 + ts.b = 3.14 + + saved = ts.to_json_compat() + assert saved['a'] == 5 + assert saved['b'] == 3.14 + assert saved['__obj_type'] == 'SimpleClass' + + +def test_list_attr(): + + cwl = ClassWithList(10, [1, 5, 2, 8]) + + saved = cwl.to_json_compat() + assert saved['x'] == 10 + assert saved['lst'] == [1, 5, 2, 8] + assert saved['__obj_type'] == 'ClassWithList' + + +def test_nested(nested_example): + + saved = nested_example.to_json_compat() + + assert saved['x'] == 34 + assert len(saved['lst']) == 3 + for obj in saved['lst']: + assert obj['__obj_type'] == 'SimpleClass' + + +def test_save_load_simple(): + sc = SimpleClass(5, 3.14) + + jc = sc.to_json_compat() + + # re-create it from the dict: + sc2 = SimpleClass.from_json_dict(jc) + + assert sc == sc2 + + +def test_save_load_nested(nested_example): + + jc = nested_example.to_json_compat() + + # re-create it from the dict: + nested_example2 = ClassWithList.from_json_dict(jc) + + assert nested_example == nested_example2 + + +def test_from_json_dict(nested_example): + + j_dict = nested_example.to_json_compat() + + reconstructed = js.from_json_dict(j_dict) + + assert reconstructed == nested_example + + +def test_from_json(nested_example): + """ + can it be re-created from an actual json string? + """ + + json_str = nested_example.to_json() + + reconstructed = js.from_json(json_str) + + assert reconstructed == nested_example + + +def test_from_json_file(nested_example): + """ + can it be re-created from an actual json file? + """ + + json_str = nested_example.to_json() + with open("temp.json", 'w') as tempfile: + tempfile.write(nested_example.to_json()) + + with open("temp.json") as tempfile: + reconstructed = js.from_json(tempfile) + + reconstructed = js.from_json(json_str) + + assert reconstructed == nested_example + + +def test_dict(): + """ + a simple class with a dict attribute + """ + cwd = ClassWithDict(45, {"this": 34, "that": 12}) + + # see if it can be reconstructed + + jc = cwd.to_json_compat() + + # re-create it from the dict: + cwd2 = ClassWithDict.from_json_dict(jc) + + assert cwd == cwd2 + + +def test_from_json_dict2(nested_dict): + """ + can it be re-created from an actual json string? + """ + + json_str = nested_dict.to_json() + reconstructed = js.from_json(json_str) + + assert reconstructed == nested_dict + + +def test_eq(): + sc1 = SimpleClass(3, 4.5) + sc2 = SimpleClass(3, 4.5) + + assert sc1 == sc2 + + +def test_not_eq(): + sc1 = SimpleClass(3, 4.5) + sc2 = SimpleClass(3, 4.4) + + assert sc1 != sc2 + + +def test_not_eq_reconstruct(): + sc1 = SimpleClass.from_json_dict(SimpleClass(3, 4.5).to_json_compat()) + sc2 = SimpleClass.from_json_dict(SimpleClass(2, 4.5).to_json_compat()) + + assert sc1 != sc2 + assert sc2 != sc1 + + +def test_not_valid(): + """ + You should get an error trying to make a savable class with + no savable attributes. + """ + with pytest.raises(TypeError): + @js.json_save + class NotValid(): + pass + + +def test_not_valid_class_not_instance(): + """ + You should get an error trying to make a savable class with + no savable attributes. + """ + with pytest.raises(TypeError): + @js.json_save + class NotValid(): + a = js.Int + b = js.Float diff --git a/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/test/test_json_save_meta.py b/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/test/test_json_save_meta.py new file mode 100644 index 0000000..2f42c22 --- /dev/null +++ b/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/test/test_json_save_meta.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 + +""" +tests for json_save +""" + +import json_save.json_save_meta as js + +import pytest + + +class NoInit(js.JsonSaveable): + x = js.Int() + y = js.String() + + +# A few simple examples to test +class SimpleClass(js.JsonSaveable): + + a = js.Int() + b = js.Float() + + def __init__(self, a=None, b=None): + if a is not None: + self.a = a + if b is not None: + self.b = b + + +class ClassWithList(js.JsonSaveable): + + x = js.Int() + lst = js.List() + + def __init__(self, x, lst): + self.x = x + self.lst = lst + + +class ClassWithDict(js.JsonSaveable): + x = js.Int() + d = js.Dict() + + def __init__(self, x, d): + self.x = x + self.d = d + + +@pytest.fixture +def nested_example(): + lst = [SimpleClass(3, 4.5), + SimpleClass(100, 5.2), + SimpleClass(34, 89.1), + ] + + return ClassWithList(34, lst) + + +@pytest.fixture +def nested_dict(): + d = {'this': SimpleClass(3, 4.5), + 'that': SimpleClass(100, 5.2), + 'other': SimpleClass(34, 89.1), + } + + return ClassWithDict(34, d) + + +def test_hasattr(): + ts = NoInit() + # has the attributes even though no __init__ exists + # they should be the default values + assert ts.x == 0 + assert ts.y == "" + + +def test_simple_save(): + + ts = SimpleClass() + ts.a = 5 + ts.b = 3.14 + + saved = ts.to_json_compat() + assert saved['a'] == 5 + assert saved['b'] == 3.14 + assert saved['__obj_type'] == 'SimpleClass' + + +def test_list_attr(): + + cwl = ClassWithList(10, [1, 5, 2, 8]) + + saved = cwl.to_json_compat() + assert saved['x'] == 10 + assert saved['lst'] == [1, 5, 2, 8] + assert saved['__obj_type'] == 'ClassWithList' + + +def test_nested(nested_example): + + saved = nested_example.to_json_compat() + + assert saved['x'] == 34 + assert len(saved['lst']) == 3 + for obj in saved['lst']: + assert obj['__obj_type'] == 'SimpleClass' + + +def test_save_load_simple(): + sc = SimpleClass(5, 3.14) + + jc = sc.to_json_compat() + + # re-create it from the dict: + sc2 = SimpleClass.from_json_dict(jc) + + assert sc == sc2 + + +def test_save_load_nested(nested_example): + + jc = nested_example.to_json_compat() + + # re-create it from the dict: + nested_example2 = ClassWithList.from_json_dict(jc) + + assert nested_example == nested_example2 + + +def test_from_json_dict(nested_example): + + j_dict = nested_example.to_json_compat() + + reconstructed = js.from_json_dict(j_dict) + + assert reconstructed == nested_example + + +def test_from_json(nested_example): + """ + can it be re-created from an actual json string? + """ + + json_str = nested_example.to_json() + + reconstructed = js.from_json(json_str) + + assert reconstructed == nested_example + + +def test_from_json_file(nested_example): + """ + can it be re-created from an actual json file? + """ + + json_str = nested_example.to_json() + with open("temp.json", 'w') as tempfile: + tempfile.write(nested_example.to_json()) + + with open("temp.json") as tempfile: + reconstructed = js.from_json(tempfile) + + reconstructed = js.from_json(json_str) + + assert reconstructed == nested_example + + +def test_dict(): + """ + a simple class with a dict attribute + """ + cwd = ClassWithDict(45, {"this": 34, "that": 12}) + + # see if it can be reconstructed + + jc = cwd.to_json_compat() + + # re-create it from the dict: + cwd2 = ClassWithDict.from_json_dict(jc) + + assert cwd == cwd2 + + +def test_from_json_dict2(nested_dict): + """ + can it be re-created from an actual json string? + """ + + json_str = nested_dict.to_json() + + reconstructed = js.from_json(json_str) + + assert reconstructed == nested_dict + +def test_eq(): + sc1 = SimpleClass(3, 4.5) + sc2 = SimpleClass(3, 4.5) + + assert sc1 == sc2 + + +def test_not_eq(): + sc1 = SimpleClass(3, 4.5) + sc2 = SimpleClass(3, 4.4) + + assert sc1 != sc2 + + +def test_not_eq_reconstruct(): + sc1 = SimpleClass.from_json_dict(SimpleClass(3, 4.5).to_json_compat()) + sc2 = SimpleClass.from_json_dict(SimpleClass(2, 4.5).to_json_compat()) + + assert sc1 != sc2 + assert sc2 != sc1 + + +def test_not_valid(): + """ + You should get an error trying to make a savable class with + no savable attributes. + """ + with pytest.raises(TypeError): + class NotValid(js.JsonSaveable): + pass + + +def test_not_valid_class_not_instance(): + """ + You should get an error trying to make a savable class with + no savable attributes. + """ + with pytest.raises(TypeError): + class NotValid(js.JsonSaveable): + a = js.Int + b = js.Float diff --git a/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/test/test_savables.py b/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/test/test_savables.py new file mode 100644 index 0000000..831374c --- /dev/null +++ b/Student/rdrovdahl/lesson04/mailroom/json_save/json_save/test/test_savables.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python + +""" +tests for the savable objects +""" +import pytest + +import json + +from json_save.saveables import * + +# The simple, almost json <-> python ones: +# Type, default, example +basics = [(String, "This is a string"), + (Int, 23), + (Float, 3.1458), + (Bool, True), + (Bool, False), + (List, [2, 3, 4]), + (Tuple, (1, 2, 3.4, "this")), + (List, [[1, 2, 3], [4, 5, 6]]), + (List, [{"3": 34}, {"4": 5}]), # list with dicts in it. + (Dict, {"this": {"3": 34}, "that": {"4": 5}}) # dict with dicts + ] + + +@pytest.mark.parametrize(('Type', 'val'), basics) +def test_basics(Type, val): + js = json.dumps(Type.to_json_compat(val)) + val2 = Type.to_python(json.loads(js)) + assert val == val2 + assert type(val) == type(val2) + + +nested = [(List, [(1, 2), (3, 4), (5, 6)]), # tuple in list + (Tuple, ((1, 2), (3, 4), (5, 6))), # tuple in tuple + ] + + +# This maybe should be fixed in the future?? +@pytest.mark.xfail(reason="nested not-standard types not supported") +@pytest.mark.parametrize(('Type', 'val'), nested) +def test_nested(Type, val): + print("original value:", val) + js = json.dumps(Type.to_json_compat(val)) + print("js is:", js) + val2 = Type.to_python(json.loads(js)) + print("new value is:", val2) + assert val == val2 + assert type(val) == type(val2) + + + + +dicts = [{"this": 14, "that": 1.23}, + {34: 15, 23: 5}, + {3.4: "float_key", 1.2: "float_key"}, + {(1, 2, 3): "tuple_key"}, + {(3, 4, 5): "tuple_int", ("this", "that"): "tuple_str"}, + {4: "int_key", 1.23: "float_key", (1, 2, 3): "tuple_key"}, + ] + + +@pytest.mark.parametrize('val', dicts) +def test_dicts(val): + js = json.dumps(Dict.to_json_compat(val)) + val2 = Dict.to_python(json.loads(js)) + assert val == val2 + assert type(val) == type(val2) + # check that the types of the keys is the same + for k1, k2 in zip(val.keys(), val2.keys()): + assert type(k1) is type(k2) + + +# These are dicts that can't be saved +# -- mixing string and non-string keys +bad_dicts = [{"this": "string_key", 4: "int_key"}, + {3: "int_key", "this": "string_key"}, + {None: "none_key", "this": "string_key"}, + {"this": "string_key", None: "none_key"}, + ] + + +@pytest.mark.parametrize("val", bad_dicts) +def test_bad_dicts(val): + with pytest.raises(TypeError): + Dict.to_json_compat(val) diff --git a/Student/rdrovdahl/lesson04/mailroom/json_save/setup.py b/Student/rdrovdahl/lesson04/mailroom/json_save/setup.py new file mode 100644 index 0000000..d85dd95 --- /dev/null +++ b/Student/rdrovdahl/lesson04/mailroom/json_save/setup.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python + +""" +This is about as simple a setup.py as you can have + +But its enough to support the json_save package + +""" + +import os + +from setuptools import setup, find_packages + + +def get_version(): + """ + Reads the version string from the package __init__ and returns it + """ + with open(os.path.join("json_save", "__init__.py")) as init_file: + for line in init_file: + parts = line.strip().partition("=") + if parts[0].strip() == "__version__": + return parts[2].strip().strip("'").strip('"') + return None + + +setup( + name='json_save', + version=get_version(), + author='Chris Barker', + author_email='PythonCHB@gmail.com', + packages=find_packages(), + # license='LICENSE.txt', + description='Metaclass based system for saving object to JSON', + long_description=open('README.txt').read(), +) diff --git a/Student/rdrovdahl/lesson04/mailroom/mail_db.json b/Student/rdrovdahl/lesson04/mailroom/mail_db.json new file mode 100644 index 0000000..af4923d --- /dev/null +++ b/Student/rdrovdahl/lesson04/mailroom/mail_db.json @@ -0,0 +1,11 @@ +{ + "__obj_type": "DonorDB", + "name": "Red Skull", + "donations": [ + 1000, + 2000, + 3000 + ], + "status": "Villian", + "location": "Europe" +} \ No newline at end of file diff --git a/Student/rdrovdahl/lesson04/mailroom/mail_db2.json b/Student/rdrovdahl/lesson04/mailroom/mail_db2.json new file mode 100644 index 0000000..af4923d --- /dev/null +++ b/Student/rdrovdahl/lesson04/mailroom/mail_db2.json @@ -0,0 +1,11 @@ +{ + "__obj_type": "DonorDB", + "name": "Red Skull", + "donations": [ + 1000, + 2000, + 3000 + ], + "status": "Villian", + "location": "Europe" +} \ No newline at end of file diff --git a/Student/rdrovdahl/lesson04/mailroom/mailroom_META.py b/Student/rdrovdahl/lesson04/mailroom/mailroom_META.py new file mode 100644 index 0000000..65f45c7 --- /dev/null +++ b/Student/rdrovdahl/lesson04/mailroom/mailroom_META.py @@ -0,0 +1,78 @@ +#! /usr/local/bin/python3 +''' +this code shows how to use metapgramming with a decorator +the json_save module will allow saving an object to a JSON file and the reading +of a JSON file to create an object + +this is a snippet of the code used in the mailroom program developed in the +first class series +''' + +import sys +sys.path.insert(0, './json_save') +import json_save.json_save_dec as js + + +donors_db_dict = {'Iron Man': ('Iron Man', ([100000, 50000, 1000], 'Hero', 'USA')), + 'Thor': ('Thor', ([50, 25, 100], 'Hero', 'Earth')), + 'Hulk': ('Hulk', ([500], 'Unaffiliated', 'Unknown')), + 'Winter Soldier': ('Winter Soldier', ([360, 480], 'Villian', 'USSR')), + 'Captain America': ('Captain America', ([30, 40], 'Hero', 'USA')), + 'Nick Fury': ('Nick Fury', ([100000, 545, 1000], 'Retired', 'Unknown')), + 'Hawkeye': ('Hawkeye', ([75, 50, 20], 'Hero', 'USA')), + 'Ultron': ('Ultron', ([50000, 40000, 50000], 'Villian', 'DarkWeb')), + 'Black Panther': ('Black Panther', ([100, 900, 50], 'Unaffiliated', 'Africa')), + 'War Machine': ('War Machine', ([10, 10], 'Unaffiliated', 'USA')), + 'Red Skull': ('Red Skull', ([1000, 2000, 3000], 'Villian', 'Europe')) + } + + +@js.json_save +class DonorDB(): + name = js.String() + donations = js.List() + status = js.String() + location = js.String() + + def __init__(self, name=None, donations=None, status=None, location=None): + self.name = name + self.donations = [] if donations is None else donations + self.status = status + self.location = location + + +def write_object_to_file(my_object): + with open("mail_db.json", 'w') as tempfile: # Can change 'w' to 'a' but the from_json doesn't like the formatting + tempfile.write(my_object.to_json()) + +def read_object_from_file(my_file): + with open(my_file) as tempfile: + reconstructed_donor = js.from_json(tempfile) + assert reconstructed_donor.name == 'Red Skull' + +if __name__ == '__main__': + dd = {} + for x in donors_db_dict.items(): + name = (x[1][0]) + # instantiate an object for every donor in the dictionary + dd[name] = DonorDB(name, x[1][1][0], x[1][1][1], x[1][1][2]) + # now write the objects to a JSON file + # found that the JSON file is readable by the from_json method with + # only a single object entry in it + # as a result, the file will be rewritten with every pass through this + # loop and only contain the last entry + write_object_to_file(dd[name]) + # show the dictionary of instances + for k, v in dd.items(): + print(k, v) + # now we can get specific instance details as follows: + # print(dd['Thor'].donations) + # print(dd['Thor'].status) + # example of how to set attribute values + # dd['Black Panther'].donations = [100, 200, 100] + # example of how to append donation values to existing list + # dd['Black Panther'].donations.append(555) + + # the following is a call to read a JSON file which has a single object + # for a donor named 'Red Skull' + read_object_from_file('mail_db2.json') diff --git a/Student/rdrovdahl/lesson04/mangler.py b/Student/rdrovdahl/lesson04/mangler.py new file mode 100644 index 0000000..6e65d1d --- /dev/null +++ b/Student/rdrovdahl/lesson04/mangler.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 + +""" +Simple metaclass example that creates upper and lower case versions of +all non-dunder class attributes + +modified such that setting an attribute f.x also sets f.xx +""" + + +class NameMangler(type): # deriving from type makes it a metaclass. + + def __new__(cls, clsname, bases, _dict): + uppercase_attr = {} + for name, val in _dict.items(): + if not name.startswith('__'): + uppercase_attr[name.upper()] = val + uppercase_attr[name.upper()*2] = val + uppercase_attr[name.lower()] = val + uppercase_attr[name.lower()*2] = val + else: + uppercase_attr[name] = val + + return super().__new__(cls, clsname, bases, uppercase_attr) + + +class Foo(metaclass=NameMangler): + x = 1 + Y = 2 + + +# note that it works for methods, too! +class Bar(metaclass=NameMangler): + x = 1 + + def a_method(self): + print("in a_method") + + +if __name__ == "__main__": + f = Foo() + print(f.x) + print(f.X) + print(f.y) + print(f.Y) + + b = Bar() + b.A_METHOD() diff --git a/Student/rdrovdahl/lesson04/mangler_dec.py b/Student/rdrovdahl/lesson04/mangler_dec.py new file mode 100644 index 0000000..3b6651a --- /dev/null +++ b/Student/rdrovdahl/lesson04/mangler_dec.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 + +""" +class decorator that adds both upper and lower case versions of +class attributes. + +Same as the NameMangler metaclass, but with a class decorator instead + +Usage example: + +@name_mangler +class Foo: + x = 1 + +f = Foo() +print(f.x) +print(f.X) +""" + + +def name_mangler(cls): + """ + Class decorator that adds upper and lower case names to the + decorated class + """ + # get the dictionary of class attributes + att_dict = vars(cls) + # create a new dict to hold the attributes + new_attrs = {} + # loop thorough all the class attributes + for name, val in att_dict.items(): + # skip all the "dunder" attributes + if not name.startswith("__"): + # Create both upper and lower case versions of all non-dunder names + # They are stored in the new_attrs dict, as you can't + # update the class namespace while looping through it. + new_attrs[name.upper()] = val + new_attrs[name.lower()] = val + # Add the new names to the cls attributes + # you can't directly update the __dict__ -- class __dict__s are not + # writable. + for name, val in new_attrs.items(): + setattr(cls, name, val) + return cls + + +@name_mangler +class Foo: + x = 1 + Y = 2 + + +# note that it works for methods, too! +@name_mangler +class Bar: + x = 1 + + def a_method(self): + print("in a_method") + + +if __name__ == "__main__": + f = Foo() + print(f.x) + print(f.X) + print(f.y) + print(f.Y) + + b = Bar() + b.A_METHOD() diff --git a/Student/rdrovdahl/lesson04/singleton.py b/Student/rdrovdahl/lesson04/singleton.py new file mode 100644 index 0000000..890082b --- /dev/null +++ b/Student/rdrovdahl/lesson04/singleton.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 + +""" +example of using __metaclass__ to impliment the singleton pattern +""" + + +class Singleton(type): + instance = None + + def __call__(cls, *args, **kwargs): + if cls.instance is None: + cls.instance = super().__call__(*args, **kwargs) + return cls.instance + + +class MyClass(metaclass=Singleton): + pass + +object1 = MyClass() +object2 = MyClass() + +print(id(object1)) +print(id(object2)) diff --git a/Student/rdrovdahl/lesson05/2018-06-10.log b/Student/rdrovdahl/lesson05/2018-06-10.log new file mode 100644 index 0000000..35abaab --- /dev/null +++ b/Student/rdrovdahl/lesson05/2018-06-10.log @@ -0,0 +1,6 @@ +2018-06-10 21:53:51,361 simple.py:65 WARNING The value of i is 50. +2018-06-10 21:53:51,361 simple.py:69 ERROR Tried to divide by zero. Var i was 50. Recovered gracefully. +2018-06-10 21:53:53,757 simple.py:65 WARNING The value of i is 50. +2018-06-10 21:53:53,757 simple.py:69 ERROR Tried to divide by zero. Var i was 50. Recovered gracefully. +2018-06-10 21:53:56,839 simple.py:65 WARNING The value of i is 50. +2018-06-10 21:53:56,839 simple.py:69 ERROR Tried to divide by zero. Var i was 50. Recovered gracefully. diff --git a/Student/rdrovdahl/lesson05/__pycache__/logging.cpython-36.pyc b/Student/rdrovdahl/lesson05/__pycache__/logging.cpython-36.pyc new file mode 100644 index 0000000..d93b437 Binary files /dev/null and b/Student/rdrovdahl/lesson05/__pycache__/logging.cpython-36.pyc differ diff --git a/Student/rdrovdahl/lesson05/debugging troubleshooting.txt b/Student/rdrovdahl/lesson05/debugging troubleshooting.txt new file mode 100644 index 0000000..095714f --- /dev/null +++ b/Student/rdrovdahl/lesson05/debugging troubleshooting.txt @@ -0,0 +1,173 @@ +Launching program in debugging mode with a variable value of ’10’: +Ryans-MacBook-Pro:lesson05 rdrovdahl$ python3 -m pdb recursive_debug.py 10 +> /Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson05/recursive_debug.py(6)() +-> ''' + + +Performing a long list to display the whole program: +(Pdb) ll + 1 #! /usr/local/bin/python3 + 2 + 3 + 4 ''' + 5 use this program to exercise debugging techniques + 6 -> ''' + 7 + 8 import sys + 9 + 10 + 11 def my_fun(n): + 12 if n == 2: + 13 return True + 14 return my_fun(n/2) + 15 + 16 if __name__ == '__main__': + 17 n = int(sys.argv[1]) + 18 print(my_fun(n)) + + +Stepping through the program to find where error is occurring: +(Pdb) n +> /Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson05/recursive_debug.py(8)() +-> import sys +(Pdb) n +> /Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson05/recursive_debug.py(11)() +-> def my_fun(n): +(Pdb) n +> /Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson05/recursive_debug.py(16)() +-> if __name__ == '__main__': +(Pdb) n +> /Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson05/recursive_debug.py(17)() +-> n = int(sys.argv[1]) +(Pdb) n +> /Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson05/recursive_debug.py(18)() +-> print(my_fun(n)) +(Pdb) n +Traceback (most recent call last): + File "/anaconda3/lib/python3.6/pdb.py", line 1667, in main + pdb._runscript(mainpyfile) + File "/anaconda3/lib/python3.6/pdb.py", line 1548, in _runscript + self.run(statement) + File "/anaconda3/lib/python3.6/bdb.py", line 431, in run + exec(cmd, globals, locals) + File "", line 1, in + File "/Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson05/recursive_debug.py", line 18, in + print(my_fun(n)) + File "/Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson05/recursive_debug.py", line 14, in my_fun + return my_fun(n/2) + File "/Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson05/recursive_debug.py", line 14, in my_fun + return my_fun(n/2) + File "/Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson05/recursive_debug.py", line 14, in my_fun + return my_fun(n/2) + [Previous line repeated 980 more times] + File "/Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson05/recursive_debug.py", line 11, in my_fun + def my_fun(n): + File "/anaconda3/lib/python3.6/bdb.py", line 50, in trace_dispatch + return self.dispatch_call(frame, arg) + File "/anaconda3/lib/python3.6/bdb.py", line 76, in dispatch_call + if not (self.stop_here(frame) or self.break_anywhere(frame)): + File "/anaconda3/lib/python3.6/bdb.py", line 173, in break_anywhere + return self.canonic(frame.f_code.co_filename) in self.breaks + File "/anaconda3/lib/python3.6/bdb.py", line 29, in canonic + if filename == "<" + filename[1:-1] + ">": +RecursionError: maximum recursion depth exceeded in comparison +Uncaught exception. Entering post mortem debugging +Running 'cont' or 'step' will restart the program +> /anaconda3/lib/python3.6/bdb.py(29)canonic() +-> if filename == "<" + filename[1:-1] + ">": +As per above, the error is the maximum recursion depth exceeded in the my_fun function + +Restarting and stepping into the my_fun function and showing the value of n every time recursion occurs: +Ryans-MacBook-Pro:lesson05 rdrovdahl$ python3 -m pdb recursive_debug.py 10 +> /Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson05/recursive_debug.py(6)() +-> ''' +(Pdb) ll + 1 #! /usr/local/bin/python3 + 2 + 3 + 4 ''' + 5 use this program to exercise debugging techniques + 6 -> ''' + 7 + 8 import sys + 9 + 10 + 11 def my_fun(n): + 12 if n == 2: + 13 return True + 14 return my_fun(n/2) + 15 + 16 if __name__ == '__main__': + 17 n = int(sys.argv[1]) + 18 print(my_fun(n)) +(Pdb) n +> /Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson05/recursive_debug.py(8)() +-> import sys +(Pdb) n +> /Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson05/recursive_debug.py(11)() +-> def my_fun(n): +(Pdb) n +> /Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson05/recursive_debug.py(16)() +-> if __name__ == '__main__': +(Pdb) n +> /Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson05/recursive_debug.py(17)() +-> n = int(sys.argv[1]) +(Pdb) n +> /Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson05/recursive_debug.py(18)() +-> print(my_fun(n)) +(Pdb) s +--Call-- +> /Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson05/recursive_debug.py(11)my_fun() +-> def my_fun(n): +(Pdb) pp n +10 +(Pdb) n +> /Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson05/recursive_debug.py(12)my_fun() +-> if n == 2: +(Pdb) n +> /Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson05/recursive_debug.py(14)my_fun() +-> return my_fun(n/2) +(Pdb) s +--Call-- +> /Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson05/recursive_debug.py(11)my_fun() +-> def my_fun(n): +(Pdb) pp n +5.0 +(Pdb) n +> /Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson05/recursive_debug.py(12)my_fun() +-> if n == 2: +(Pdb) n +> /Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson05/recursive_debug.py(14)my_fun() +-> return my_fun(n/2) +(Pdb) s +--Call-- +> /Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson05/recursive_debug.py(11)my_fun() +-> def my_fun(n): +(Pdb) pp n +2.5 +(Pdb) n +> /Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson05/recursive_debug.py(12)my_fun() +-> if n == 2: +(Pdb) n +> /Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson05/recursive_debug.py(14)my_fun() +-> return my_fun(n/2) +(Pdb) s +--Call-- +> /Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson05/recursive_debug.py(11)my_fun() +-> def my_fun(n): +(Pdb) pp n +1.25 +(Pdb) +At this point, the value of n is 1.25 which is less than 2. 2 is the only value which will stop the recursive loop. +N will continue to decrease in value with each recursive loop and has no way of ever reaching 2 which is why we are seeing the recursive maximum depth error. + +The issue with the function is that it only works with variables which are factors of 2. + + + + + + + + + diff --git a/Student/rdrovdahl/lesson05/pysyslog.py b/Student/rdrovdahl/lesson05/pysyslog.py new file mode 100644 index 0000000..49f6831 --- /dev/null +++ b/Student/rdrovdahl/lesson05/pysyslog.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python + +## Tiny Syslog Server in Python. +## +## This is a tiny syslog server that is able to receive UDP based syslog +## entries on a specified port and save them to a file. +## That's it... it does nothing else... +## There are a few configuration parameters. + +LOG_FILE = 'syslog.log' +HOST, PORT = "127.0.0.1", 514 + +# +# NO USER SERVICEABLE PARTS BELOW HERE... +# + +import logging +import socketserver + +logging.basicConfig(level=logging.INFO, format='%(message)s', datefmt='', filename=LOG_FILE, filemode='a') + +class SyslogUDPHandler(socketserver.BaseRequestHandler): + + def handle(self): + data = bytes.decode(self.request[0].strip()) + socket = self.request[1] + print( "%s : " % self.client_address[0], str(data)) + logging.info(str(data)) + +if __name__ == "__main__": + try: + server = socketserver.UDPServer((HOST,PORT), SyslogUDPHandler) + server.serve_forever(poll_interval=0.5) + except (IOError, SystemExit): + raise + except KeyboardInterrupt: + print ("Crtl+C Pressed. Shutting down.") diff --git a/Student/rdrovdahl/lesson05/recursive_debug.py b/Student/rdrovdahl/lesson05/recursive_debug.py new file mode 100644 index 0000000..5d5c5ac --- /dev/null +++ b/Student/rdrovdahl/lesson05/recursive_debug.py @@ -0,0 +1,18 @@ +#! /usr/local/bin/python3 + + +''' +use this program to exercise debugging techniques +''' + +import sys + + +def my_fun(n): + if n == 2: + return True + return my_fun(n/2) + +if __name__ == '__main__': + n = int(sys.argv[1]) + print(my_fun(n)) diff --git a/Student/rdrovdahl/lesson05/simple.py b/Student/rdrovdahl/lesson05/simple.py new file mode 100644 index 0000000..97ef684 --- /dev/null +++ b/Student/rdrovdahl/lesson05/simple.py @@ -0,0 +1,72 @@ +#! /usr/local/bin/python3 + +''' +To complete this assignment, modify simple.py to satisfy the following goals: + +* You want ALL log messages logged to the console. The format of these messages + should include the current time. + +* You want WARNING and higher messages logged to a file named { todays-date }.log. + The format of these messages should include the current time. + +* You want ERROR and higher messages logged to a syslog server. The syslog server + will be appending its own time stamps to the messages that it receives, so DO + NOT include the current time in the format of the log messages that you send + to the server. + +To complete this assignment, you will need to create: + +* A second instance of Formatter. Because the three different destinations for + your log messages require two different formats (one with time stamps and one + without), you'll need two different instances of Formatter. + +* A third Handler, to send messages to the syslog server. +''' + + +import logging +import logging.handlers +from datetime import datetime + +# format for console and logfile logs +format = "%(asctime)s %(filename)s:%(lineno)-3d %(levelname)s %(message)s" +# format for syslog messages +# added time/date stamp as syslog server being used doesn't stamp messages +format2 = "%(asctime)s %(lineno)-3d %(levelname)s %(message)s" + +# formatter for console and logfile logging +formatter = logging.Formatter(format) +# formatter for syslog logging +formatter2 = logging.Formatter(format2) + +file_handler = logging.FileHandler('{:%Y-%m-%d}.log'.format(datetime.now())) #need to change the logfile name format +file_handler.setLevel(logging.WARNING) +file_handler.setFormatter(formatter) + +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) + +syslog_handler = logging.handlers.SysLogHandler(address=('127.0.0.1', 514)) +syslog_handler.setLevel(logging.ERROR) +syslog_handler.setFormatter(formatter2) + + +logger = logging.getLogger() +logger.setLevel(logging.DEBUG) +logger.addHandler(file_handler) +logger.addHandler(console_handler) +logger.addHandler(syslog_handler) + +def my_fun(n): + for i in range(0, n): + logging.debug(i) + if i == 50: + logging.warning("The value of i is 50.") + try: + i / (50 - i) + except ZeroDivisionError: + logging.error("Tried to divide by zero. Var i was {}. Recovered gracefully.".format(i)) + +if __name__ == "__main__": + my_fun(100) diff --git a/Student/rdrovdahl/lesson05/syslog.log b/Student/rdrovdahl/lesson05/syslog.log new file mode 100644 index 0000000..00dfcfa Binary files /dev/null and b/Student/rdrovdahl/lesson05/syslog.log differ diff --git a/Student/rdrovdahl/lesson06/calculator assignment results.rtf b/Student/rdrovdahl/lesson06/calculator assignment results.rtf new file mode 100644 index 0000000..d582090 --- /dev/null +++ b/Student/rdrovdahl/lesson06/calculator assignment results.rtf @@ -0,0 +1,162 @@ +{\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf400 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;\f1\fnil\fcharset0 Menlo-Regular;} +{\colortbl;\red255\green255\blue255;\red239\green239\blue239;\red4\green4\blue4;} +{\*\expandedcolortbl;;\cssrgb\c94962\c94962\c94962;\cssrgb\c1294\c1294\c1294;} +\margl1440\margr1440\vieww12600\viewh7800\viewkind0 +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 + +\f0\fs24 \cf0 Lesson06 Test Results\ +\ +Unit Testing:\ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0 + +\f1\fs22 \cf2 \cb3 \CocoaLigature0 Ryans-MacBook-Pro:calculator rdrovdahl$ python3 -m unittest test\ +........\ +----------------------------------------------------------------------\ +Ran 8 tests in 0.003s\ +\ +OK\ +Ryans-MacBook-Pro:calculator rdrovdahl$ +\f0\fs24 \cf0 \cb1 \CocoaLigature1 \ +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 +\cf0 \ +Pylint Testing:\ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0 + +\f1\fs22 \cf2 \cb3 \CocoaLigature0 Report\ +======\ +39 statements analysed.\ +\ +Statistics by type\ +------------------\ +\ ++---------+-------+-----------+-----------+------------+---------+\ +|type |number |old number |difference |%documented |%badname |\ ++=========+=======+===========+===========+============+=========+\ +|module |7 |7 |= |100.00 |0.00 |\ ++---------+-------+-----------+-----------+------------+---------+\ +|class |6 |6 |= |100.00 |0.00 |\ ++---------+-------+-----------+-----------+------------+---------+\ +|method |11 |11 |= |100.00 |0.00 |\ ++---------+-------+-----------+-----------+------------+---------+\ +|function |0 |0 |= |0 |0 |\ ++---------+-------+-----------+-----------+------------+---------+\ +\ +\ +\ +External dependencies\ +---------------------\ +::\ +\ + calculator \ + \\-exceptions (calculator.calculator)\ +\ +\ +\ +Raw metrics\ +-----------\ +\ ++----------+-------+------+---------+-----------+\ +|type |number |% |previous |difference |\ ++==========+=======+======+=========+===========+\ +|code |56 |37.58 |56 |= |\ ++----------+-------+------+---------+-----------+\ +|docstring |71 |47.65 |71 |= |\ ++----------+-------+------+---------+-----------+\ +|comment |0 |0.00 |0 |= |\ ++----------+-------+------+---------+-----------+\ +|empty |22 |14.77 |22 |= |\ ++----------+-------+------+---------+-----------+\ +\ +\ +\ +Duplication\ +-----------\ +\ ++-------------------------+------+---------+-----------+\ +| |now |previous |difference |\ ++=========================+======+=========+===========+\ +|nb duplicated lines |0 |0 |= |\ ++-------------------------+------+---------+-----------+\ +|percent duplicated lines |0.000 |0.000 |= |\ ++-------------------------+------+---------+-----------+\ +\ +\ +\ +Messages by category\ +--------------------\ +\ ++-----------+-------+---------+-----------+\ +|type |number |previous |difference |\ ++===========+=======+=========+===========+\ +|convention |0 |0 |= |\ ++-----------+-------+---------+-----------+\ +|refactor |0 |4 |-4.00 |\ ++-----------+-------+---------+-----------+\ +|warning |0 |0 |= |\ ++-----------+-------+---------+-----------+\ +|error |0 |0 |= |\ ++-----------+-------+---------+-----------+\ +\ +\ +\ +\ +-------------------------------------------------------------------\ +Your code has been rated at 10.00/10 (previous run: 8.97/10, +1.03)\ +\ +Ryans-MacBook-Pro:calculator rdrovdahl$ +\f0\fs24 \cf0 \cb1 \CocoaLigature1 \ +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 +\cf0 \ +\ +Flake8 Testing:\ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0 + +\f1\fs22 \cf2 \cb3 \CocoaLigature0 Ryans-MacBook-Pro:calculator rdrovdahl$ flake8 calculate\ +Ryans-MacBook-Pro:calculator rdrovdahl$ +\f0\fs24 \cf0 \cb1 \CocoaLigature1 \ +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 +\cf0 \ +\ +Coverage:\ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0 + +\f1\fs22 \cf2 \cb3 \CocoaLigature0 Ryans-MacBook-Pro:calculator rdrovdahl$ coverage report\ +Name Stmts Miss Cover\ +----------------------------------------------\ +calculator/__init__.py 0 0 100%\ +calculator/adder.py 3 0 100%\ +calculator/calculator.py 25 0 100%\ +calculator/divider.py 3 0 100%\ +calculator/exceptions.py 2 0 100%\ +calculator/multiplier.py 3 0 100%\ +calculator/subtracter.py 3 0 100%\ +----------------------------------------------\ +TOTAL 39 0 100%\ +Ryans-MacBook-Pro:calculator rdrovdahl$ coverage report -m\ +Name Stmts Miss Cover Missing\ +--------------------------------------------------------\ +calculator/__init__.py 0 0 100%\ +calculator/adder.py 3 0 100%\ +calculator/calculator.py 25 0 100%\ +calculator/divider.py 3 0 100%\ +calculator/exceptions.py 2 0 100%\ +calculator/multiplier.py 3 0 100%\ +calculator/subtracter.py 3 0 100%\ +--------------------------------------------------------\ +TOTAL 39 0 100%\ +Ryans-MacBook-Pro:calculator rdrovdahl$ python3 -m unittest test\ +........\ +----------------------------------------------------------------------\ +Ran 8 tests in 0.003s\ +\ +OK\ +Ryans-MacBook-Pro:calculator rdrovdahl$ +\f0\fs24 \cf0 \cb1 \CocoaLigature1 \ +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 +\cf0 \ +\ +\ +\ +\ +} \ No newline at end of file diff --git a/Student/rdrovdahl/lesson06/calculator/.coverage b/Student/rdrovdahl/lesson06/calculator/.coverage new file mode 100644 index 0000000..b8fe66e --- /dev/null +++ b/Student/rdrovdahl/lesson06/calculator/.coverage @@ -0,0 +1 @@ +!coverage.py: This is a private format, don't read it directly!{"lines":{"/Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson06/calculator/calculator/__init__.py":[1],"/Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson06/calculator/calculator/adder.py":[3,6,9,10,15],"/Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson06/calculator/calculator/subtracter.py":[3,6,9,10,15],"/Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson06/calculator/calculator/multiplier.py":[3,6,9,10,15],"/Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson06/calculator/calculator/divider.py":[3,6,9,10,15],"/Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson06/calculator/calculator/calculator.py":[3,5,8,12,14,25,31,43,49,55,61,18,19,20,21,23,29,47,36,37,38,39,40,41,53,59,65],"/Users/rdrovdahl/gitroot/SP2018-Python220-Accelerated/Student/rdrovdahl/lesson06/calculator/calculator/exceptions.py":[3,6,9,10]}} \ No newline at end of file diff --git a/Student/rdrovdahl/lesson06/calculator/__pycache__/adder.cpython-36.pyc b/Student/rdrovdahl/lesson06/calculator/__pycache__/adder.cpython-36.pyc new file mode 100644 index 0000000..f8677bb Binary files /dev/null and b/Student/rdrovdahl/lesson06/calculator/__pycache__/adder.cpython-36.pyc differ diff --git a/Student/rdrovdahl/lesson06/calculator/__pycache__/calculator.cpython-36.pyc b/Student/rdrovdahl/lesson06/calculator/__pycache__/calculator.cpython-36.pyc new file mode 100644 index 0000000..a6e57f4 Binary files /dev/null and b/Student/rdrovdahl/lesson06/calculator/__pycache__/calculator.cpython-36.pyc differ diff --git a/Student/rdrovdahl/lesson06/calculator/__pycache__/divider.cpython-36.pyc b/Student/rdrovdahl/lesson06/calculator/__pycache__/divider.cpython-36.pyc new file mode 100644 index 0000000..5c7ce07 Binary files /dev/null and b/Student/rdrovdahl/lesson06/calculator/__pycache__/divider.cpython-36.pyc differ diff --git a/Student/rdrovdahl/lesson06/calculator/__pycache__/exceptions.cpython-36.pyc b/Student/rdrovdahl/lesson06/calculator/__pycache__/exceptions.cpython-36.pyc new file mode 100644 index 0000000..96e4e8c Binary files /dev/null and b/Student/rdrovdahl/lesson06/calculator/__pycache__/exceptions.cpython-36.pyc differ diff --git a/Student/rdrovdahl/lesson06/calculator/__pycache__/multiplier.cpython-36.pyc b/Student/rdrovdahl/lesson06/calculator/__pycache__/multiplier.cpython-36.pyc new file mode 100644 index 0000000..c1e8325 Binary files /dev/null and b/Student/rdrovdahl/lesson06/calculator/__pycache__/multiplier.cpython-36.pyc differ diff --git a/Student/rdrovdahl/lesson06/calculator/__pycache__/subtracter.cpython-36.pyc b/Student/rdrovdahl/lesson06/calculator/__pycache__/subtracter.cpython-36.pyc new file mode 100644 index 0000000..0031856 Binary files /dev/null and b/Student/rdrovdahl/lesson06/calculator/__pycache__/subtracter.cpython-36.pyc differ diff --git a/Student/rdrovdahl/lesson06/calculator/__pycache__/test.cpython-36.pyc b/Student/rdrovdahl/lesson06/calculator/__pycache__/test.cpython-36.pyc new file mode 100644 index 0000000..3bada92 Binary files /dev/null and b/Student/rdrovdahl/lesson06/calculator/__pycache__/test.cpython-36.pyc differ diff --git a/Student/rdrovdahl/lesson06/calculator/calculator/.pylintrc b/Student/rdrovdahl/lesson06/calculator/calculator/.pylintrc new file mode 100644 index 0000000..6ffba3e --- /dev/null +++ b/Student/rdrovdahl/lesson06/calculator/calculator/.pylintrc @@ -0,0 +1,540 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. +jobs=1 + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Specify a configuration file. +#rcfile= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=too-few-public-methods, + print-statement, + parameter-unpacking, + unpacking-in-except, + old-raise-syntax, + backtick, + long-suffix, + old-ne-operator, + old-octal-literal, + import-star-module-level, + non-ascii-bytes-literal, + raw-checker-failed, + bad-inline-option, + locally-disabled, + locally-enabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + apply-builtin, + basestring-builtin, + buffer-builtin, + cmp-builtin, + coerce-builtin, + execfile-builtin, + file-builtin, + long-builtin, + raw_input-builtin, + reduce-builtin, + standarderror-builtin, + unicode-builtin, + xrange-builtin, + coerce-method, + delslice-method, + getslice-method, + setslice-method, + no-absolute-import, + old-division, + dict-iter-method, + dict-view-method, + next-method-called, + metaclass-assignment, + indexing-exception, + raising-string, + reload-builtin, + oct-method, + hex-method, + nonzero-method, + cmp-method, + input-builtin, + round-builtin, + intern-builtin, + unichr-builtin, + map-builtin-not-iterating, + zip-builtin-not-iterating, + range-builtin-not-iterating, + filter-builtin-not-iterating, + using-cmp-argument, + eq-without-hash, + div-method, + idiv-method, + rdiv-method, + exception-message-attribute, + invalid-str-codec, + sys-max-int, + bad-python3-import, + deprecated-string-function, + deprecated-str-translate-call, + deprecated-itertools-function, + deprecated-types-field, + next-method-defined, + dict-items-not-iterating, + dict-keys-not-iterating, + dict-values-not-iterating + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio).You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=optparse.Values,sys.exit + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma, + dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[BASIC] + +# Naming style matching correct argument names +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style +#argument-rgx= + +# Naming style matching correct attribute names +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Naming style matching correct class attribute names +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style +#class-attribute-rgx= + +# Naming style matching correct class names +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming-style +#class-rgx= + +# Naming style matching correct constant names +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma +good-names=i, + j, + k, + ex, + Run, + _ + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# Naming style matching correct inline iteration names +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style +#inlinevar-rgx= + +# Naming style matching correct method names +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style +#method-rgx= + +# Naming style matching correct module names +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style +#variable-rgx= + + +[IMPORTS] + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in a if statement +max-bool-expr=5 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of statements in function / method body +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/Student/rdrovdahl/lesson06/calculator/calculator/__init__.py b/Student/rdrovdahl/lesson06/calculator/calculator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Student/rdrovdahl/lesson06/calculator/calculator/__pycache__/__init__.cpython-36.pyc b/Student/rdrovdahl/lesson06/calculator/calculator/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000..69e65a3 Binary files /dev/null and b/Student/rdrovdahl/lesson06/calculator/calculator/__pycache__/__init__.cpython-36.pyc differ diff --git a/Student/rdrovdahl/lesson06/calculator/calculator/__pycache__/adder.cpython-36.pyc b/Student/rdrovdahl/lesson06/calculator/calculator/__pycache__/adder.cpython-36.pyc new file mode 100644 index 0000000..28e830c Binary files /dev/null and b/Student/rdrovdahl/lesson06/calculator/calculator/__pycache__/adder.cpython-36.pyc differ diff --git a/Student/rdrovdahl/lesson06/calculator/calculator/__pycache__/calculator.cpython-36.pyc b/Student/rdrovdahl/lesson06/calculator/calculator/__pycache__/calculator.cpython-36.pyc new file mode 100644 index 0000000..643068d Binary files /dev/null and b/Student/rdrovdahl/lesson06/calculator/calculator/__pycache__/calculator.cpython-36.pyc differ diff --git a/Student/rdrovdahl/lesson06/calculator/calculator/__pycache__/divider.cpython-36.pyc b/Student/rdrovdahl/lesson06/calculator/calculator/__pycache__/divider.cpython-36.pyc new file mode 100644 index 0000000..086193f Binary files /dev/null and b/Student/rdrovdahl/lesson06/calculator/calculator/__pycache__/divider.cpython-36.pyc differ diff --git a/Student/rdrovdahl/lesson06/calculator/calculator/__pycache__/exceptions.cpython-36.pyc b/Student/rdrovdahl/lesson06/calculator/calculator/__pycache__/exceptions.cpython-36.pyc new file mode 100644 index 0000000..42ca603 Binary files /dev/null and b/Student/rdrovdahl/lesson06/calculator/calculator/__pycache__/exceptions.cpython-36.pyc differ diff --git a/Student/rdrovdahl/lesson06/calculator/calculator/__pycache__/multiplier.cpython-36.pyc b/Student/rdrovdahl/lesson06/calculator/calculator/__pycache__/multiplier.cpython-36.pyc new file mode 100644 index 0000000..7f4d123 Binary files /dev/null and b/Student/rdrovdahl/lesson06/calculator/calculator/__pycache__/multiplier.cpython-36.pyc differ diff --git a/Student/rdrovdahl/lesson06/calculator/calculator/__pycache__/subtracter.cpython-36.pyc b/Student/rdrovdahl/lesson06/calculator/calculator/__pycache__/subtracter.cpython-36.pyc new file mode 100644 index 0000000..5b5c305 Binary files /dev/null and b/Student/rdrovdahl/lesson06/calculator/calculator/__pycache__/subtracter.cpython-36.pyc differ diff --git a/Student/rdrovdahl/lesson06/calculator/calculator/adder.py b/Student/rdrovdahl/lesson06/calculator/calculator/adder.py new file mode 100644 index 0000000..4b53354 --- /dev/null +++ b/Student/rdrovdahl/lesson06/calculator/calculator/adder.py @@ -0,0 +1,15 @@ +''' +This module provides an addition operator +''' + + +class Adder(): + ''' + addition class + ''' + @staticmethod + def calc(operand_1, operand_2): + ''' + calculation method + ''' + return operand_1 + operand_2 diff --git a/Student/rdrovdahl/lesson06/calculator/calculator/calculator.py b/Student/rdrovdahl/lesson06/calculator/calculator/calculator.py new file mode 100644 index 0000000..d3ce153 --- /dev/null +++ b/Student/rdrovdahl/lesson06/calculator/calculator/calculator.py @@ -0,0 +1,65 @@ +''' +main calculator program +''' + +from .exceptions import InsufficientOperands + + +class Calculator(): + ''' + demonstrating architecture of: + Dependency Inversion or Dependency Injection + ''' + + def __init__(self, adder, subtracter, multiplier, divider): + ''' + instantiates an instance of a calculator + ''' + self.adder = adder + self.subtracter = subtracter + self.multiplier = multiplier + self.divider = divider + + self.stack = [] + + def enter_number(self, number): + ''' + enter a number into the calculator stack + ''' + self.stack.insert(0, number) + + def _do_calc(self, operator): + ''' + function that will call the appropriate class and to retrieve the + result + ''' + try: + result = operator.calc(self.stack[1], self.stack[0]) + self.stack = [result] + return result + except IndexError: + raise InsufficientOperands + + def add(self): + ''' + add method + ''' + return self._do_calc(self.adder) + + def subtract(self): + ''' + subtraction method + ''' + return self._do_calc(self.subtracter) + + def multiply(self): + ''' + multiplication method + ''' + return self._do_calc(self.multiplier) + + def divide(self): + ''' + division method + ''' + return self._do_calc(self.divider) diff --git a/Student/rdrovdahl/lesson06/calculator/calculator/divider.py b/Student/rdrovdahl/lesson06/calculator/calculator/divider.py new file mode 100644 index 0000000..fb549f8 --- /dev/null +++ b/Student/rdrovdahl/lesson06/calculator/calculator/divider.py @@ -0,0 +1,15 @@ +''' +This module provides a division operator +''' + + +class Divider(): + ''' + division class + ''' + @staticmethod + def calc(operand_1, operand_2): + ''' + calculation method + ''' + return operand_1 / operand_2 diff --git a/Student/rdrovdahl/lesson06/calculator/calculator/exceptions.py b/Student/rdrovdahl/lesson06/calculator/calculator/exceptions.py new file mode 100644 index 0000000..ff6e824 --- /dev/null +++ b/Student/rdrovdahl/lesson06/calculator/calculator/exceptions.py @@ -0,0 +1,10 @@ +''' +custom errors for calculator program +''' + + +class InsufficientOperands(Exception): + ''' + use when less than 2 operands are provided to an equation method + ''' + pass diff --git a/Student/rdrovdahl/lesson06/calculator/calculator/multiplier.py b/Student/rdrovdahl/lesson06/calculator/calculator/multiplier.py new file mode 100644 index 0000000..4c80cab --- /dev/null +++ b/Student/rdrovdahl/lesson06/calculator/calculator/multiplier.py @@ -0,0 +1,15 @@ +''' +This module provides a multiplication operator +''' + + +class Multiplier(): + ''' + multiplication class + ''' + @staticmethod + def calc(operand_1, operand_2): + ''' + calculation method + ''' + return operand_1 * operand_2 diff --git a/Student/rdrovdahl/lesson06/calculator/calculator/subtracter.py b/Student/rdrovdahl/lesson06/calculator/calculator/subtracter.py new file mode 100644 index 0000000..8ec2c1c --- /dev/null +++ b/Student/rdrovdahl/lesson06/calculator/calculator/subtracter.py @@ -0,0 +1,15 @@ +''' +This module provides a subtraction operator +''' + + +class Subtracter(): + ''' + subtraction class + ''' + @staticmethod + def calc(operand_1, operand_2): + ''' + calculation method + ''' + return operand_1 - operand_2 diff --git a/Student/rdrovdahl/lesson06/calculator/test.py b/Student/rdrovdahl/lesson06/calculator/test.py new file mode 100644 index 0000000..387574e --- /dev/null +++ b/Student/rdrovdahl/lesson06/calculator/test.py @@ -0,0 +1,104 @@ +''' +unit tests for the calculator program +to run from command line: +'python -m unittest test' +''' +from unittest import TestCase +from unittest.mock import MagicMock + +from calculator.adder import Adder +from calculator.subtracter import Subtracter +from calculator.multiplier import Multiplier +from calculator.divider import Divider +from calculator.calculator import Calculator +from calculator.exceptions import InsufficientOperands + + +class AdderTests(TestCase): + def test_adding(self): + adder = Adder() + for i in range(-10, 10): + for j in range(-10, 10): + self.assertEqual(i + j, adder.calc(i, j)) + + +class SubtracterTests(TestCase): + def test_subtracting(self): + subtracter = Subtracter() + for i in range(-10, 10): + for j in range(-10, 10): + self.assertEqual(i - j, subtracter.calc(i, j)) + + +class CalculatorTests(TestCase): + # setup is run each time a test method is run - BEFORE that test method + # is run + def setUp(self): + self.adder = Adder() + self.subtracter = Subtracter() + self.multiplier = Multiplier() + self.divider = Divider() + self.calculator = Calculator(self.adder, self.subtracter, + self.multiplier, self.divider) + + def test_insufficient_operands(self): + self.calculator.enter_number(0) + with self.assertRaises(InsufficientOperands): + self.calculator.add() + + def test_adder_call(self): + # use Mock to tell python to always return '0' when the adder method + # is called + # this test is not testing the adder function, so we don't care what + # value is returned + # using a mock value focuses the testing on the calculator class which + # is the unit being tested here + self.adder.calc = MagicMock(return_value=0) + + self.calculator.enter_number(1) + self.calculator.enter_number(2) + self.calculator.add() + + self.adder.calc.assert_called_with(1, 2) + + def test_subtracter_call(self): + self.subtracter.calc = MagicMock(return_value=0) + + self.calculator.enter_number(1) + self.calculator.enter_number(2) + self.calculator.subtract() + + self.subtracter.calc.assert_called_with(1, 2) + + +class ModuleTests(TestCase): + def test_module(self): + calculator = Calculator(Adder(), Subtracter(), Multiplier(), Divider()) + calculator.enter_number(5) + calculator.enter_number(2) + calculator.multiply() + calculator.enter_number(46) + calculator.add() + calculator.enter_number(8) + calculator.divide() + calculator.enter_number(1) + result = calculator.subtract() + self.assertEqual(6, result) + + +class MultiplierTests(TestCase): + def test_multiplying(self): + multiplier = Multiplier() + for i in range(-10, 10): + for j in range(-10, 10): + self.assertEqual(i * j, multiplier.calc(i, j)) + + +class DividerTests(TestCase): + def test_dividing(self): + divider = Divider() + for i in range(-10, 10): + for j in range(-10, 10): + if j == 0: + break + self.assertEqual(i / j, divider.calc(i, j)) diff --git a/Student/rdrovdahl/lesson07/mailroom_RDB/QuickDBD-mailroom RDB.png b/Student/rdrovdahl/lesson07/mailroom_RDB/QuickDBD-mailroom RDB.png new file mode 100644 index 0000000..093784f Binary files /dev/null and b/Student/rdrovdahl/lesson07/mailroom_RDB/QuickDBD-mailroom RDB.png differ diff --git a/Student/rdrovdahl/lesson07/mailroom_RDB/__pycache__/mailroom_db_model.cpython-36.pyc b/Student/rdrovdahl/lesson07/mailroom_RDB/__pycache__/mailroom_db_model.cpython-36.pyc new file mode 100644 index 0000000..14e7609 Binary files /dev/null and b/Student/rdrovdahl/lesson07/mailroom_RDB/__pycache__/mailroom_db_model.cpython-36.pyc differ diff --git a/Student/rdrovdahl/lesson07/mailroom_RDB/letters/Black_Panther.06-23-2018.txt b/Student/rdrovdahl/lesson07/mailroom_RDB/letters/Black_Panther.06-23-2018.txt new file mode 100644 index 0000000..dc1f0fe --- /dev/null +++ b/Student/rdrovdahl/lesson07/mailroom_RDB/letters/Black_Panther.06-23-2018.txt @@ -0,0 +1,12 @@ +Dear Black Panther, + We at the Avengers Fund-a-Kitten Initiative would like to thank you for + your generous donation of $50.00. + + Taking advantage of our kitten matching partner, with these added funds we + will be able to provide 20.00 kitten(s) to well deserving little girls + all over the world including hard to reach places like Antarctica and + Tacoma, WA! + + + Sincerely, + Your Friends at AFAK diff --git a/Student/rdrovdahl/lesson07/mailroom_RDB/letters/Captain_America.06-23-2018.txt b/Student/rdrovdahl/lesson07/mailroom_RDB/letters/Captain_America.06-23-2018.txt new file mode 100644 index 0000000..70b30cc --- /dev/null +++ b/Student/rdrovdahl/lesson07/mailroom_RDB/letters/Captain_America.06-23-2018.txt @@ -0,0 +1,12 @@ +Dear Captain America, + We at the Avengers Fund-a-Kitten Initiative would like to thank you for + your generous donation of $40.00. + + Taking advantage of our kitten matching partner, with these added funds we + will be able to provide 16.00 kitten(s) to well deserving little girls + all over the world including hard to reach places like Antarctica and + Tacoma, WA! + + + Sincerely, + Your Friends at AFAK diff --git a/Student/rdrovdahl/lesson07/mailroom_RDB/letters/Hawkeye.06-23-2018.txt b/Student/rdrovdahl/lesson07/mailroom_RDB/letters/Hawkeye.06-23-2018.txt new file mode 100644 index 0000000..56dee39 --- /dev/null +++ b/Student/rdrovdahl/lesson07/mailroom_RDB/letters/Hawkeye.06-23-2018.txt @@ -0,0 +1,12 @@ +Dear Hawkeye, + We at the Avengers Fund-a-Kitten Initiative would like to thank you for + your generous donation of $20.00. + + Taking advantage of our kitten matching partner, with these added funds we + will be able to provide 8.00 kitten(s) to well deserving little girls + all over the world including hard to reach places like Antarctica and + Tacoma, WA! + + + Sincerely, + Your Friends at AFAK diff --git a/Student/rdrovdahl/lesson07/mailroom_RDB/letters/Iron_Man.06-23-2018.txt b/Student/rdrovdahl/lesson07/mailroom_RDB/letters/Iron_Man.06-23-2018.txt new file mode 100644 index 0000000..3e778a9 --- /dev/null +++ b/Student/rdrovdahl/lesson07/mailroom_RDB/letters/Iron_Man.06-23-2018.txt @@ -0,0 +1,12 @@ +Dear Iron Man, + We at the Avengers Fund-a-Kitten Initiative would like to thank you for + your generous donation of $1,000.00. + + Taking advantage of our kitten matching partner, with these added funds we + will be able to provide 400.00 kitten(s) to well deserving little girls + all over the world including hard to reach places like Antarctica and + Tacoma, WA! + + + Sincerely, + Your Friends at AFAK diff --git a/Student/rdrovdahl/lesson07/mailroom_RDB/letters/Mr_X.06-23-2018.txt b/Student/rdrovdahl/lesson07/mailroom_RDB/letters/Mr_X.06-23-2018.txt new file mode 100644 index 0000000..a9b375a --- /dev/null +++ b/Student/rdrovdahl/lesson07/mailroom_RDB/letters/Mr_X.06-23-2018.txt @@ -0,0 +1,12 @@ +Dear Mr X, + We at the Avengers Fund-a-Kitten Initiative would like to thank you for + your generous donation of $400.00. + + Taking advantage of our kitten matching partner, with these added funds we + will be able to provide 160.00 kitten(s) to well deserving little girls + all over the world including hard to reach places like Antarctica and + Tacoma, WA! + + + Sincerely, + Your Friends at AFAK diff --git a/Student/rdrovdahl/lesson07/mailroom_RDB/letters/New_Guy.06-23-2018.txt b/Student/rdrovdahl/lesson07/mailroom_RDB/letters/New_Guy.06-23-2018.txt new file mode 100644 index 0000000..1640517 --- /dev/null +++ b/Student/rdrovdahl/lesson07/mailroom_RDB/letters/New_Guy.06-23-2018.txt @@ -0,0 +1,12 @@ +Dear New Guy, + We at the Avengers Fund-a-Kitten Initiative would like to thank you for + your generous donation of $0.00. + + Taking advantage of our kitten matching partner, with these added funds we + will be able to provide 0.00 kitten(s) to well deserving little girls + all over the world including hard to reach places like Antarctica and + Tacoma, WA! + + + Sincerely, + Your Friends at AFAK diff --git a/Student/rdrovdahl/lesson07/mailroom_RDB/letters/Nick_Fury.06-23-2018.txt b/Student/rdrovdahl/lesson07/mailroom_RDB/letters/Nick_Fury.06-23-2018.txt new file mode 100644 index 0000000..a62eb00 --- /dev/null +++ b/Student/rdrovdahl/lesson07/mailroom_RDB/letters/Nick_Fury.06-23-2018.txt @@ -0,0 +1,12 @@ +Dear Nick Fury, + We at the Avengers Fund-a-Kitten Initiative would like to thank you for + your generous donation of $1,000.00. + + Taking advantage of our kitten matching partner, with these added funds we + will be able to provide 400.00 kitten(s) to well deserving little girls + all over the world including hard to reach places like Antarctica and + Tacoma, WA! + + + Sincerely, + Your Friends at AFAK diff --git a/Student/rdrovdahl/lesson07/mailroom_RDB/letters/Red_Skull.06-23-2018.txt b/Student/rdrovdahl/lesson07/mailroom_RDB/letters/Red_Skull.06-23-2018.txt new file mode 100644 index 0000000..cdb994e --- /dev/null +++ b/Student/rdrovdahl/lesson07/mailroom_RDB/letters/Red_Skull.06-23-2018.txt @@ -0,0 +1,12 @@ +Dear Red Skull, + We at the Avengers Fund-a-Kitten Initiative would like to thank you for + your generous donation of $3,000.00. + + Taking advantage of our kitten matching partner, with these added funds we + will be able to provide 1,200.00 kitten(s) to well deserving little girls + all over the world including hard to reach places like Antarctica and + Tacoma, WA! + + + Sincerely, + Your Friends at AFAK diff --git a/Student/rdrovdahl/lesson07/mailroom_RDB/letters/Thor.06-23-2018.txt b/Student/rdrovdahl/lesson07/mailroom_RDB/letters/Thor.06-23-2018.txt new file mode 100644 index 0000000..610bebe --- /dev/null +++ b/Student/rdrovdahl/lesson07/mailroom_RDB/letters/Thor.06-23-2018.txt @@ -0,0 +1,12 @@ +Dear Thor, + We at the Avengers Fund-a-Kitten Initiative would like to thank you for + your generous donation of $100.00. + + Taking advantage of our kitten matching partner, with these added funds we + will be able to provide 40.00 kitten(s) to well deserving little girls + all over the world including hard to reach places like Antarctica and + Tacoma, WA! + + + Sincerely, + Your Friends at AFAK diff --git a/Student/rdrovdahl/lesson07/mailroom_RDB/letters/Ultron.06-23-2018.txt b/Student/rdrovdahl/lesson07/mailroom_RDB/letters/Ultron.06-23-2018.txt new file mode 100644 index 0000000..fa77c1c --- /dev/null +++ b/Student/rdrovdahl/lesson07/mailroom_RDB/letters/Ultron.06-23-2018.txt @@ -0,0 +1,12 @@ +Dear Ultron, + We at the Avengers Fund-a-Kitten Initiative would like to thank you for + your generous donation of $50,000.00. + + Taking advantage of our kitten matching partner, with these added funds we + will be able to provide 20,000.00 kitten(s) to well deserving little girls + all over the world including hard to reach places like Antarctica and + Tacoma, WA! + + + Sincerely, + Your Friends at AFAK diff --git a/Student/rdrovdahl/lesson07/mailroom_RDB/letters/War_Machine.06-23-2018.txt b/Student/rdrovdahl/lesson07/mailroom_RDB/letters/War_Machine.06-23-2018.txt new file mode 100644 index 0000000..9bb94dc --- /dev/null +++ b/Student/rdrovdahl/lesson07/mailroom_RDB/letters/War_Machine.06-23-2018.txt @@ -0,0 +1,12 @@ +Dear War Machine, + We at the Avengers Fund-a-Kitten Initiative would like to thank you for + your generous donation of $10.00. + + Taking advantage of our kitten matching partner, with these added funds we + will be able to provide 4.00 kitten(s) to well deserving little girls + all over the world including hard to reach places like Antarctica and + Tacoma, WA! + + + Sincerely, + Your Friends at AFAK diff --git a/Student/rdrovdahl/lesson07/mailroom_RDB/letters/Winter_Soldier.06-23-2018.txt b/Student/rdrovdahl/lesson07/mailroom_RDB/letters/Winter_Soldier.06-23-2018.txt new file mode 100644 index 0000000..77a7416 --- /dev/null +++ b/Student/rdrovdahl/lesson07/mailroom_RDB/letters/Winter_Soldier.06-23-2018.txt @@ -0,0 +1,12 @@ +Dear Winter Soldier, + We at the Avengers Fund-a-Kitten Initiative would like to thank you for + your generous donation of $480.00. + + Taking advantage of our kitten matching partner, with these added funds we + will be able to provide 192.00 kitten(s) to well deserving little girls + all over the world including hard to reach places like Antarctica and + Tacoma, WA! + + + Sincerely, + Your Friends at AFAK diff --git a/Student/rdrovdahl/lesson07/mailroom_RDB/mailroom class details.rtf b/Student/rdrovdahl/lesson07/mailroom_RDB/mailroom class details.rtf new file mode 100644 index 0000000..8412f3a --- /dev/null +++ b/Student/rdrovdahl/lesson07/mailroom_RDB/mailroom class details.rtf @@ -0,0 +1,349 @@ +{\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf400 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;\f1\fnil\fcharset0 Menlo-Regular;\f2\fnil\fcharset0 Menlo-Bold; +} +{\colortbl;\red255\green255\blue255;\red46\green151\blue31;\red4\green4\blue4;\red48\green232\blue26; +\red239\green239\blue239;\red163\green37\blue27;\red252\green35\blue25;\red196\green6\blue255;\red194\green102\blue28; +} +{\*\expandedcolortbl;;\cssrgb\c20961\c64158\c15777;\cssrgb\c1294\c1294\c1294;\cssrgb\c18251\c90714\c13124; +\cssrgb\c94962\c94962\c94962;\cssrgb\c70575\c21649\c13659;\cssrgb\c100000\c23715\c11979;\cssrgb\c81919\c23714\c100000;\cssrgb\c80989\c47739\c14240; +} +\margl1440\margr1440\vieww17620\viewh23080\viewkind0 +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 + +\f0\fs24 \cf0 \ul \ulc0 This guide is for the original class based mailroom solution. It will be modified as mailroom is refactored to use databases\ulnone \ +\ +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 + +\b \cf0 def get_sample_data():\ + +\b0 return [Donor('Iron Man', [10, 20, 30], 'Hero', 'USA),\ + Donor('Thor', .....\ +\ + +\b db = DonorDB(get_sample_data())\ + +\b0 This command accomplishes two things:\ + 1 - A complete set of Donor() class objects is created for each item in the persistent donor list\ + 2 - A dictionary object is created in the DonorDB() class which has a key/value pair for each Donor() object. The keys are the normalized names and the values are the associated Donor() class objects.\ + +\b \ +\ + Donor() attributes:\ + name\ + donations\ + affiliation\ + location\ + Donor() properties\ + total_donations\ + average_donations\ + Donor() methods:\ + normalize_name\ + add_donation\ +\ + DonorDB() attributes:\ + donor_data\ + DonorDB() properties:\ + donors\ + DonorDB() methods:\ + list_donors\ + find_donor\ + add_donor\ + gen_letter\ + save_letters_to_disk\ + sort_key\ + generate_donor_report\ +\ +class Donor()\ + __init__(self, name, donations, affiliation, location):\ + +\b0 self.norm_name = self.normalize_name(name)\ + self.name = name.strip()\ + self.donations = [] or list(donations)\ + self.affiliation = affiliation\ + self.location = location\ +\ + @staticmethod\ + +\b normalize_name(name):\ + +\b0 return...\ +\ + @property\ + +\b total_donations(self):\ + +\b0 return...\ +\ + @property\ + +\b average_donations(self):\ + +\b0 return...\ + \ + +\b add_donation(self, amount):\ + +\b0 self.donations.append(amount)\ +\ +\ + +\b class DonorDB():\ + __init__(self, donors):\ + +\b0 self.donor_data = \{\} or \{d.norm_name: d for d in donors\}\ + \ + @property\ + +\b donors(self):\ + +\b0 return self.donor_data.values()\ +\ + +\b list_donors(self):\ + +\b0 listing = ["Donor list:"]\ + for donor in self.donors:\ + listing.append(donor.name)\ + return "\\n".join(listing)\ +\ + +\b find_donor(self, name)\ + +\b0 return self.donor_data.get(Donor.normalize_name(name))\ +\ + +\b add_donor(self, name):\ + +\b0 donor = Donor(name) # creates the new object in the Donor() class\ + self.donor_data[donor.norm_name] = donor # adds the new object to the donor_data dictionary\ + return donor # return Donor() object for new name\ +\ + +\b gen_letter(self, donor):\ + +\b0 return letter\ + \ + +\b save_letters_to_disk(self):\ + +\b0 creates \'91./letters\'92 directory\ + iterates through self.donor_data.values() and creates letters for each donor using donor.name calling on self.gen_letter(donor) for the content\ + no return\ +\ + @staticmethod\ + +\b sort_key(item):\ + +\b0 sort key used in the generate_donor_report() method\ +\ + +\b generate_donor_report(self):\ + +\b0 returns a donors report sorted by total donations\ +\ + \ +\ + +\b DonorDB() Class Examples:\ + +\b0 In the __init__ method, self.donor_data is defined as a dictionary of objects where:\ + the key is the object.norm_name\ + the value is the object +\b \ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0 + +\f1\b0\fs22 \cf2 \cb3 \CocoaLigature0 In [ +\f2\b \cf4 47 +\f1\b0 \cf2 ]: \cf5 db.donor_data\ +\cf6 Out[ +\f2\b \cf7 47 +\f1\b0 \cf6 ]: \cf5 \ +\{'blackpanther': <__main__.Donor at 0x1032ed048>,\ + 'captainamerica': <__main__.Donor at 0x1033b99b0>,\ + 'hawkeye': <__main__.Donor at 0x1033b9d68>,\ + 'ironman': <__main__.Donor at 0x1033b9b00>,\ + 'nickfury': <__main__.Donor at 0x1033b9748>,\ + 'redskull': <__main__.Donor at 0x1032ed0f0>,\ + 'thor': <__main__.Donor at 0x1033b9f28>,\ + 'ultron': <__main__.Donor at 0x1033b9898>,\ + 'warmachine': <__main__.Donor at 0x1032ed128>,\ + 'wintersoldier': <__main__.Donor at 0x1033b9e80>\} +\f0\fs24 \cf0 \cb1 \CocoaLigature1 \ +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 +\cf0 \ +\ +The donors property method returns a list of the db.donor_data values. Essentially, this is just a list of Donor class objects\ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0 + +\f1\fs22 \cf2 \cb3 \CocoaLigature0 In [ +\f2\b \cf4 49 +\f1\b0 \cf2 ]: +\f2\b for +\f1\b0 \cf5 d +\f2\b \cf8 in +\f1\b0 \cf5 db.donors:\ +\cf2 ...: \cf5 \cf2 print\cf5 (d)\ +\cf2 ...: \cf5 \ +<__main__.Donor object at 0x1033b9b00>\ +<__main__.Donor object at 0x1033b9f28>\ +<__main__.Donor object at 0x1033b9e80>\ +<__main__.Donor object at 0x1033b99b0>\ +<__main__.Donor object at 0x1033b9748>\ +<__main__.Donor object at 0x1033b9d68>\ +<__main__.Donor object at 0x1033b9898>\ +<__main__.Donor object at 0x1032ed048>\ +<__main__.Donor object at 0x1032ed128>\ +<__main__.Donor object at 0x1032ed0f0>\ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0 + +\f0\fs24 \cf0 \cb1 \CocoaLigature1 \ +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 +\cf0 The list_donors method iterates through the db.donors property and returns a list of name attributes from the Donor class objects\ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0 + +\f1\fs22 \cf2 \cb3 \CocoaLigature0 In [ +\f2\b \cf4 50 +\f1\b0 \cf2 ]: \cf5 db.list_donors()\ +\cf6 Out[ +\f2\b \cf7 50 +\f1\b0 \cf6 ]: \cf5 'Donor list:\\nIron Man\\nThor\\nWinter Soldier\\nCaptain America\\nNick Fury\\nHawkeye\\nUltron\\nBlack Panther\\nWar Machine\\nRed Skull'\ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0 + +\f0\fs24 \cf0 \cb1 \CocoaLigature1 \ +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 +\cf0 \ +The find_donor method returns the original Donors object for the name given as the argument.\ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0 + +\f1\fs22 \cf2 \cb3 \CocoaLigature0 In [ +\f2\b \cf4 69 +\f1\b0 \cf2 ]: \cf5 db.find_donor(\cf9 'Thor'\cf5 )\ +\cf6 Out[ +\f2\b \cf7 69 +\f1\b0 \cf6 ]: \cf5 <__main__.Donor at 0x1032cdf28>\ +\ +\cf2 In [ +\f2\b \cf4 70 +\f1\b0 \cf2 ]: \cf5 db.find_donor(\cf9 'Thor'\cf5 ).name\ +\cf6 Out[ +\f2\b \cf7 70 +\f1\b0 \cf6 ]: \cf5 'Thor'\ +\ +\cf2 In [ +\f2\b \cf4 71 +\f1\b0 \cf2 ]: \cf5 db.find_donor(\cf9 'Thor'\cf5 ).donations\ +\cf6 Out[ +\f2\b \cf7 71 +\f1\b0 \cf6 ]: \cf5 [50, 25, 100]\ +\ +\cf2 In [ +\f2\b \cf4 72 +\f1\b0 \cf2 ]: \cf5 db.find_donor(\cf9 'Thor'\cf5 ).affiliation\ +\cf6 Out[ +\f2\b \cf7 72 +\f1\b0 \cf6 ]: \cf5 'Hero'\ +\ +\cf2 In [ +\f2\b \cf4 73 +\f1\b0 \cf2 ]: \cf5 db.find_donor(\cf9 'Thor'\cf5 ).location\ +\cf6 Out[ +\f2\b \cf7 73 +\f1\b0 \cf6 ]: \cf5 'Earth'\ +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 + +\f0\fs24 \cf0 \cb1 \CocoaLigature1 \ +\ +Add a donor object using the add_donor() method\ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0 + +\f1\fs22 \cf2 \cb3 \CocoaLigature0 In [ +\f2\b \cf4 85 +\f1\b0 \cf2 ]: \cf5 db.add_donor(\cf9 'Mr X'\cf5 )\ +\cf6 Out[ +\f2\b \cf7 85 +\f1\b0 \cf6 ]: \cf5 <__main__.Donor at 0x103366898>\ +\ +\cf2 In [ +\f2\b \cf4 86 +\f1\b0 \cf2 ]: print\cf5 (db.list_donors())\ +Donor list:\ +Iron Man\ +Thor\ +Winter Soldier\ +Captain America\ +Nick Fury\ +Hawkeye\ +Ultron\ +Black Panther\ +War Machine\ +Red Skull\ +Mr X\ +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 + +\f0\fs24 \cf0 \cb1 \CocoaLigature1 \ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0 + +\f1\fs22 \cf2 \cb3 \CocoaLigature0 In [ +\f2\b \cf4 87 +\f1\b0 \cf2 ]: \cf5 db.find_donor(\cf9 'Mr X'\cf5 ).donations\ +\cf6 Out[ +\f2\b \cf7 87 +\f1\b0 \cf6 ]: \cf5 []\ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0 + +\f0\fs24 \cf0 \cb1 \CocoaLigature1 \ +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 +\cf0 \ +How to use db.gen_letter\ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0 + +\f1\fs22 \cf2 \cb3 \CocoaLigature0 In [ +\f2\b \cf4 119 +\f1\b0 \cf2 ]: \cf5 letter = db.gen_letter(db.find_donor(\cf9 'Thor'\cf5 ))\ +\ +\cf2 In [ +\f2\b \cf4 120 +\f1\b0 \cf2 ]: print\cf5 (letter)\ +Dear Thor,\ + Thank you for your very kind donation of $100.00.\ + It will be put to very good use.\ + Sincerely,\ + -The Team\ +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 + +\f0\fs24 \cf0 \cb1 \CocoaLigature1 \ +\ +How to use generate_donor_reoprt\ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0 + +\f1\fs22 \cf2 \cb3 \CocoaLigature0 In [ +\f2\b \cf4 5 +\f1\b0 \cf2 ]: print\cf5 (db.generate_donor_report())\ +Donor Name | Total Given | Num Gifts | Average Gift\ +------------------------------------------------------------------\ +Mr X $ 0.00 0 $ 0.00\ +War Machine $ 20.00 2 $ 10.00\ +Captain America $ 70.00 2 $ 35.00\ +Hawkeye $ 145.00 3 $ 48.33\ +Thor $ 175.00 3 $ 58.33\ +Winter Soldier $ 840.00 2 $ 420.00\ +Black Panther $ 1050.00 3 $ 350.00\ +Red Skull $ 6000.00 3 $ 2000.00\ +Nick Fury $ 101545.00 3 $ 33848.33\ +Ultron $ 140000.00 3 $ 46666.67\ +Iron Man $ 151000.00 3 $ 50333.33\ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0 + +\f0\fs24 \cf0 \cb1 \CocoaLigature1 \ +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 +\cf0 \ +\ +\ +\ +\ +\ +\ +\ +\ +\ +} \ No newline at end of file diff --git a/Student/rdrovdahl/lesson07/mailroom_RDB/mailroom.db b/Student/rdrovdahl/lesson07/mailroom_RDB/mailroom.db new file mode 100644 index 0000000..d7fe13c Binary files /dev/null and b/Student/rdrovdahl/lesson07/mailroom_RDB/mailroom.db differ diff --git a/Student/rdrovdahl/lesson07/mailroom_RDB/mailroom.py b/Student/rdrovdahl/lesson07/mailroom_RDB/mailroom.py new file mode 100644 index 0000000..66064a5 --- /dev/null +++ b/Student/rdrovdahl/lesson07/mailroom_RDB/mailroom.py @@ -0,0 +1,558 @@ +#! /usr/local/bin/python3 +""" +This is an object oriented version of the mailroom program which uses peewee +as an ORM with a sqlite database +""" + +import os +import sys +import math +# handy utility to make pretty printing easier +from textwrap import dedent +import datetime +import pathlib +# database schema +from mailroom_db_model import * + + +def populate_sql_db(): + """ + seed the sqlite database + """ + response = input('Would you like to perform the initial population of the sqlite database? y/n >') + if response.lower() == 'n': + return + donor_list = [('Iron Man', [100000, 50000, 1000], 'Hero', 'USA'), + ('Thor', [50, 25, 100], 'Hero', 'Earth'), + ('Winter Soldier', [360, 480], 'Villian', 'USSR'), + ('Captain America', [30, 40], 'Hero', 'USA'), + ('Nick Fury', [100000, 545, 1000], 'Retired', 'Unknown'), + ('Hawkeye', [75, 50, 20], 'Hero', 'USA'), + ('Ultron', [50000, 40000, 50000], 'Villian', 'DarkWeb'), + ('Black Panther', [100, 900, 50], 'Unaffiliated', 'Africa'), + ('War Machine', [10, 10], 'Unaffiliated', 'USA'), + ('Red Skull', [1000, 2000, 3000], 'Villian', 'Europe'), + ] + database = SqliteDatabase('mailroom.db') + # populate Donor table + try: + database.connect() + database.execute_sql('PRAGMA foreign_keys = ON;') + for donor in donor_list: + with database.transaction(): + new_donor = Donors.create( + donor_name=donor[0], + donor_name_normalized=donor[0].lower(), + affiliation=donor[2], + location=donor[3]) + new_donor.save() + except Exception as e: + print(e) + # populate Donations table + try: + for donor in donor_list: + id = Donors.select().where(Donors.donor_name == donor[0]).get() + for i in range(len(donor[1])): + with database.transaction(): + new_donation = Donations.create( + donor=id, + donation=donor[1][i]) + new_donation.save() + except Exception as e: + print(e) + finally: + database.close() + + + +# Utility so we have data to test with, etc. +# def get_sample_data(): +# """ +# returns a list of donor objects to use as sample data +# """ +# return [Donor('Iron Man', [100000, 50000, 1000], 'Hero', 'USA'), +# Donor('Thor', [50, 25, 100], 'Hero', 'Earth'), +# Donor('Winter Soldier', [360, 480], 'Villian', 'USSR'), +# Donor('Captain America', [30, 40], 'Hero', 'USA'), +# Donor('Nick Fury', [100000, 545, 1000], 'Retired', 'Unknown'), +# Donor('Hawkeye', [75, 50, 20], 'Hero', 'USA'), +# Donor('Ultron', [50000, 40000, 50000], 'Villian', 'DarkWeb'), +# Donor('Black Panther', [100, 900, 50], 'Unaffiliated', 'Africa'), +# Donor('War Machine', [10, 10], 'Unaffiliated', 'USA'), +# Donor('Red Skull', [1000, 2000, 3000], 'Villian', 'Europe'), +# ] + + +class Donor(): + """ + class to hold the information about a single donor + """ + + def __init__(self, name, donations=None, affiliation='unknown', location='unknown'): + """ + create a new Donor object + :param name: the full name of the donor + :param donations=None: iterable of past donations + """ + + self.norm_name = self.normalize_name(name) + self.name = name.strip() + if donations is None: + self.donations = [] + else: + self.donations = list(donations) + self.affiliation = affiliation + self.location = location + + @staticmethod + def donations_rdb(name): + query = (Donors.select(Donors, Donations).join(Donations) + .where(Donors.donor_name_normalized == name.lower())) + donations = list(d.donations.donation for d in query) + return donations + + + @staticmethod + def normalize_name(name): + """ + return a normalized version of a name to use as a comparison key + simple enough to not be in a method now, but maybe you'd want to make it fancier later. + """ + return name.lower().strip().replace(" ", "") + + # @property + # def last_donation(self): + # """ + # The most recent donation made + # """ + # try: + # return self.donations[-1] + # except IndexError: + # return None + + @staticmethod + def last_donation_rdb(name): + """ + The most recent donation made + """ + query = (Donors.select(Donors, Donations).join(Donations) + .where(Donors.donor_name_normalized == name.lower())) + donations = list(d.donations.donation for d in query) + try: + return donations[-1] + except IndexError: + return 0 + + # @property + # def total_donations(self): + # return sum(self.donations) + + @staticmethod + def total_donations_rdb(name): + query = (Donors.select(Donors, Donations).join(Donations) + .where(Donors.donor_name_normalized == name.lower())) + donations = list(d.donations.donation for d in query) + return sum(donations) + + # @property + # def average_donation(self): + # try: + # return self.total_donations / len(self.donations) + # except ZeroDivisionError: + # return 0 + + @staticmethod + def average_donation_rdb(name): + query = (Donors.select(Donors, Donations).join(Donations) + .where(Donors.donor_name_normalized == name.lower())) + donations = list(d.donations.donation for d in query) + try: + return sum(donations) / len(query) + except ZeroDivisionError: + return 0 + + # def add_donation(self, amount): + # """ + # add a new donation + # """ + # amount = float(amount) + # if amount <= 0.0: + # raise ValueError("Donation must be greater than zero") + # self.donations.append(amount) + + @staticmethod + def add_donation_rdb(name, amount): + """ + add a new donation + """ + amount = float(amount) + if amount <= 0.0: + raise ValueError("Donation must be greater than zero") + id = Donors.select().where(Donors.donor_name_normalized == name.lower()).get() + new_donation = Donations.create( + donor=id, + donation=amount) + new_donation.save() + + +class DonorDB(): + """ + encapsulation of the entire database of donors and associated data + """ + + def __init__(self, donors=None): + """ + Initialize a new donor database + :param donors=None: iterable of Donor objects + """ + if donors is None: + self.donor_data = {} + else: + self.donor_data = {d.norm_name: d for d in donors} + + # def save_to_file(self, filename): + # with open(filename, 'w') as outfile: + # self.to_json(outfile) + + # @classmethod + # def load_from_file(cls, filename): + # with open(filename, 'r') as infile: + # obj = js.from_json(infile) + # return obj + + # @property + # def donors(self): + # """ + # an iterable of all the donors + # """ + # return self.donor_data.values() + + # def list_donors(self): + # """ + # creates a list of the donors as a string, so they can be printed + # Not calling print from here makes it more flexible and easier to + # test + # """ + # listing = ["Donor list:"] + # for donor in self.donors: + # listing.append(donor.name) + # return "\n".join(listing) + + @staticmethod + def list_donors_rdb(): + """ + creates a list of the donors as a string, so they can be printed + Not calling print from here makes it more flexible and easier to + test + """ + listing = ["Donor list:"] + for donor in Donors: + listing.append(donor.donor_name) + return "\n".join(listing) + + # def find_donor(self, name): + # """ + # find a donor in the donor db + # :param: the name of the donor + # :returns: The donor data structure -- None if not in the self.donor_data + # """ + # return self.donor_data.get(Donor.normalize_name(name)) + + # def add_donor(self, name): + # """ + # Add a new donor to the donor db + # :param: the name of the donor + # :returns: the new Donor data structure + # """ + # donor = Donor(name) + # self.donor_data[donor.norm_name] = donor + # return donor + + @staticmethod + def add_donor_rdb(name, affiliation=None, location=None): + """ + Add a new donor to the donor db + :param: the name of the donor + :returns: the new donor id + """ + # check to see if donor already exists + query = Donors.select().where(Donors.donor_name_normalized==name.lower()) + if query.exists(): + return None + # add new donor if doesn't already exist + else: + try: + new_donor = Donors.create( + donor_name=name, + donor_name_normalized=name.lower(), + affiliation=affiliation, + location=location) + new_donor.save() + except Exception as e: + print(e) + id = Donors.select().where(Donors.donor_name_normalized == name.lower()).get() + return id + + + # def gen_letter(self, donor): + # """ + # Generate a thank you letter for the donor + # :param: donor tuple + # :returns: string with letter + # note: This doesn't actually write to a file -- that's a separate + # function. This makes it more flexible and easier to test. + # """ + # k = (donor.last_donation/5 * 2) + # letter = (f'''Dear {donor.name}, + # We at the Avengers Fund-a-Kitten Initiative would like to thank you for + # your generous donation of ${donor.last_donation:,.2f}.\n + # Taking advantage of our kitten matching partner, with these added funds we + # will be able to provide {k:,.2f} kitten(s) to well deserving little girls + # all over the world including hard to reach places like Antarctica and + # Tacoma, WA!\n\n + # Sincerely, + # Your Friends at AFAK\n''') + # return letter + + @staticmethod + def gen_letter_rdb(name): + """ + Generate a thank you letter for the donor + :param: donor tuple + :returns: string with letter + note: This doesn't actually write to a file -- that's a separate + function. This makes it more flexible and easier to test. + """ + k = (Donor.last_donation_rdb(name)/5 * 2) + letter = (f'''Dear {name}, + We at the Avengers Fund-a-Kitten Initiative would like to thank you for + your generous donation of ${Donor.last_donation_rdb(name):,.2f}.\n + Taking advantage of our kitten matching partner, with these added funds we + will be able to provide {k:,.2f} kitten(s) to well deserving little girls + all over the world including hard to reach places like Antarctica and + Tacoma, WA!\n\n + Sincerely, + Your Friends at AFAK\n''') + return letter + + # def save_letters_to_disk(self): + # """ + # make a letter for each donor, and save it to disk. + # """ + # # create 'letters' directory if one does not exist + # pathlib.Path('letters').mkdir(exist_ok=True) + # # set the datetime format variable + # dt_format = '.%m-%d-%Y' + # for donor in self.donor_data.values(): + # print("Writing a letter to:", donor.name) + # letter = self.gen_letter(donor) + # # I don't like spaces in filenames... + # filename = donor.name.replace(" ", "_") + # # set the file path using pathlib + # p = pathlib.Path('letters/' + filename + + # datetime.datetime.now().strftime(dt_format) + '.txt') + # open(p, 'w').write(letter) + + def save_letters_to_disk_rdb(): + """ + make a letter for each donor, and save it to disk. + """ + # create 'letters' directory if one does not exist + pathlib.Path('letters').mkdir(exist_ok=True) + # set the datetime format variable + dt_format = '.%m-%d-%Y' + for donor in Donors: + print("Writing a letter to:", donor.donor_name) + letter = DonorDB.gen_letter_rdb(donor.donor_name) + # I don't like spaces in filenames... + filename = donor.donor_name.replace(" ", "_") + # set the file path using pathlib + p = pathlib.Path('letters/' + filename + + datetime.datetime.now().strftime(dt_format) + '.txt') + open(p, 'w').write(letter) + + # @staticmethod + # def sort_key(item): + # # used to sort on name in self.donor_data + # return item[1] + + # def generate_donor_report(self): + # """ + # Generate the report of the donors and amounts donated. + # :returns: the donor report as a string. + # """ + # # First, reduce the raw data into a summary list view + # report_rows = [] + # for donor in self.donor_data.values(): + # name = donor.name + # gifts = donor.donations + # total_gifts = donor.total_donations + # num_gifts = len(gifts) + # avg_gift = donor.average_donation + # report_rows.append((name, total_gifts, num_gifts, avg_gift)) + # + # # sort the report data + # report_rows.sort(key=self.sort_key, reverse=True) + # report = [] + # report.append("{:25s} | {:11s} | {:9s} | {:12s}".format("Donor Name", + # "Total Given", + # "Num Gifts", + # "Average Gift")) + # report.append("-" * 66) + # for row in report_rows: + # report.append("{:25s} ${:10.2f} {:9d} ${:11.2f}".format(*row)) + # return "\n".join(report) + + @staticmethod + def sort_key_rdb(item): + return item[1] + + @staticmethod + def generate_donor_report_rdb(): + """ + Generate the report of the donors and amounts donated. + :returns: the donor report as a string. + """ + # First, reduce the raw data into a summary list view + report_rows = [] + for donor in Donors: + name = donor.donor_name + gifts = Donor.donations_rdb(donor.donor_name) + total_gifts = Donor.total_donations_rdb(donor.donor_name) + num_gifts = len(gifts) + avg_gift = Donor.average_donation_rdb(donor.donor_name) + report_rows.append((name, total_gifts, num_gifts, avg_gift)) + + # sort the report data + report_rows.sort(key=DonorDB.sort_key_rdb, reverse=True) + report = [] + report.append("{:25s} | {:11s} | {:9s} | {:12s}".format("Donor Name", + "Total Given", + "Num Gifts", + "Average Gift")) + report.append("-" * 66) + for row in report_rows: + report.append("{:25s} ${:10.2f} {:9d} ${:11.2f}".format(*row)) + return "\n".join(report) + +# User-interaction code +# Above this is all the logic code +# The stuff you'd need if you had a totally different UI.different +# below is code only for the command line interface. + + +# db = DonorDB(get_sample_data()) + + +def main_menu_selection(): + """ + Print out the main application menu and then read the user input. + """ + action = input(dedent(''' + Choose an action: + 1 - Send a Thank You + 2 - Create a Report + 3 - Send letters to everyone + 4 - Quit + > ''')) + return action.strip() + + +def send_thank_you(): + """ + Record a donation and generate a thank you message. + """ + # Read a valid donor to send a thank you from, handling special commands to + # let the user navigate as defined. + os.system('clear') + print('''THANK YOU Menu + +Let's record that new donation and draft a THANK YOU message.\n\n''') + while True: + print('\nChoose an action:\n') + print('1 - Start working on a donor entry\n' + '2 - Enter "list" to see a list of existing donors\n' + '3 - Enter "quit" to return to the main menu\n') + response = input(' >> ') + if response == '1': + print('\nType the name of an existing or new donor.\n' + '(type "quit" at any time to return to the main menu)') + name = input(' >> ') + if name.lower() == 'quit': + return + amount = input('What is the donation amount? \n >> ') + # normallize the donation amount + amount = amount.replace('$', '').replace(',', '') + try: + amount = float(amount) + except ValueError: + print('\nNot a valid entry. Need to enter a numerical value for the ' + 'donation amount\n') + input(' press "Enter" to return to the THANK YOU Menu ') + return + + # If this is a new user, ensure that the database has the necessary + # data structure. + # donor = db.find_donor(name) + # if donor is None: + # donor = db.add_donor(name) + DonorDB.add_donor_rdb(name) + + # Record the donation + # donor.add_donation(amount) + Donor.add_donation_rdb(name, amount) + + # Print thank you letter to screen + # print(db.gen_letter(donor)) + print(DonorDB.gen_letter_rdb(name)) + input('\n press "Enter" to return to the THANK YOU Menu ') + os.system('clear') + return + + elif response == 'list' or response == '2': + os.system('clear') + print(db.list_donors()) + elif response == 'quit' or response == '3': + os.system('clear') + return + else: + os.system('clear') + print('not a valid response, try again\n') + + +def print_donor_report(): + os.system('clear') + # print(db.generate_donor_report()) + print(DonorDB.generate_donor_report_rdb()) + +def quit(): + sys.exit(0) + + +def main(): + '''main menu function''' + os.system('clear') + print('''Avengers: Fund-a-Kitten Initiative + + Because every little girl + Everywhere in the world + ...deserves a kitten + + +Welcome to Mailroom\n\n''') + + selection_dict = {"1": send_thank_you, + "2": print_donor_report, + "3": DonorDB.save_letters_to_disk_rdb, + "4": quit} + + while True: + selection = main_menu_selection() + try: + selection_dict[selection]() + except KeyError: + print("error: menu selection is invalid!") + +if __name__ == "__main__": + populate_sql_db() + main() diff --git a/Student/rdrovdahl/lesson07/mailroom_RDB/mailroom_db_model.py b/Student/rdrovdahl/lesson07/mailroom_RDB/mailroom_db_model.py new file mode 100644 index 0000000..b6fcabb --- /dev/null +++ b/Student/rdrovdahl/lesson07/mailroom_RDB/mailroom_db_model.py @@ -0,0 +1,41 @@ +""" + Mailroom database with Peewee ORM, sqlite and Python + Here we define the schema and create the database tables +""" + +from peewee import * + +database = SqliteDatabase('mailroom.db') +database.connect() +database.execute_sql('PRAGMA foreign_keys = ON;') + +class BaseModel(Model): + class Meta: + database = database + + +class Donors(BaseModel): + """ + This class defines Donor, which maintains details of someone + for who has donated to the Avengers Fund A Kitten initiative + """ + id = PrimaryKeyField() + donor_name = CharField(max_length=30) + donor_name_normalized = CharField(max_length=30) + affiliation = CharField(max_length=30, null=True) + location = CharField(max_length=40, null=True) + + +class Donations(BaseModel): + """ + This class contains the list of donations for each Donor + """ + id = PrimaryKeyField() + donor = ForeignKeyField(Donors, backref='donations') + donation = IntegerField() + donation_date = DateField(formats='MM-DD-YYYY', null=True) + +database.create_tables([ + Donors, + Donations, + ], safe=True) diff --git a/Student/rdrovdahl/lesson07/personjob_modified/__pycache__/personjob_model.cpython-36.pyc b/Student/rdrovdahl/lesson07/personjob_modified/__pycache__/personjob_model.cpython-36.pyc new file mode 100644 index 0000000..54d96fc Binary files /dev/null and b/Student/rdrovdahl/lesson07/personjob_modified/__pycache__/personjob_model.cpython-36.pyc differ diff --git a/Student/rdrovdahl/lesson07/personjob_modified/modified personjob.py output.rtfd/Pasted Graphic.tiff b/Student/rdrovdahl/lesson07/personjob_modified/modified personjob.py output.rtfd/Pasted Graphic.tiff new file mode 100644 index 0000000..5ae9fe2 Binary files /dev/null and b/Student/rdrovdahl/lesson07/personjob_modified/modified personjob.py output.rtfd/Pasted Graphic.tiff differ diff --git a/Student/rdrovdahl/lesson07/personjob_modified/modified personjob.py output.rtfd/TXT.rtf b/Student/rdrovdahl/lesson07/personjob_modified/modified personjob.py output.rtfd/TXT.rtf new file mode 100644 index 0000000..36aae3d --- /dev/null +++ b/Student/rdrovdahl/lesson07/personjob_modified/modified personjob.py output.rtfd/TXT.rtf @@ -0,0 +1,87 @@ +{\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf400 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;\f1\fnil\fcharset0 Menlo-Regular;} +{\colortbl;\red255\green255\blue255;\red239\green239\blue239;\red4\green4\blue4;} +{\*\expandedcolortbl;;\cssrgb\c94962\c94962\c94962;\cssrgb\c1294\c1294\c1294;} +\margl1440\margr1440\vieww19020\viewh24360\viewkind0 +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 + +\f0\fs24 \cf0 Tables from sqlite3:\ +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 +\cf0 {{\NeXTGraphic Pasted Graphic.tiff \width15720 \height7340 \appleattachmentpadding0 \appleembedtype0 \appleaqc +}}\ +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 +\cf0 \ +Command line output:\ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0 + +\f1\fs22 \cf2 \cb3 \CocoaLigature0 Ryans-MacBook-Pro:personjob_modified rdrovdahl$ python3 personjob_learning.py \ +INFO:__main__:Working with Person class\ +INFO:__main__:Note how I use constants and a list of tuples as a simple schema\ +INFO:__main__:Normally you probably will have prompted for this from a user\ +INFO:__main__:Creating Person records: iterate through the list of tuples\ +INFO:__main__:Prepare to explain any errors with exceptions\ +INFO:__main__:and the transaction tells the database to fail on error\ +INFO:__main__:Database add successful\ +INFO:__main__:Database add successful\ +INFO:__main__:Database add successful\ +INFO:__main__:Database add successful\ +INFO:__main__:Database add successful\ +INFO:__main__:Print the Person records we saved...\ +INFO:__main__:Andrew lives in Sumner and likes to be known as Andy\ +INFO:__main__:Peter lives in Seattle and likes to be known as None\ +INFO:__main__:Susan lives in Boston and likes to be known as Beannie\ +INFO:__main__:Pam lives in Coventry and likes to be known as PJ\ +INFO:__main__:Steven lives in Colchester and likes to be known as None\ +INFO:__main__:Working with Department class\ +INFO:__main__:Creating Department records: iterate through the list of tuples\ +INFO:__main__:Prepare to explain any errors with exceptions\ +INFO:__main__:and the transaction tells the database to fail on error\ +INFO:__main__:Database add successful\ +INFO:__main__:Database add successful\ +INFO:__main__:Database add successful\ +INFO:__main__:Database add successful\ +INFO:__main__:Print the Department records we saved...\ +INFO:__main__:E101 is the ID for the Engineering department and the department manager is Mr Sprocket\ +INFO:__main__:T201 is the ID for the Information Technology department and the department manager is Mr Bits\ +INFO:__main__:B402 is the ID for the Business Administration department and the department manager is Mrs Pots\ +INFO:__main__:H504 is the ID for the Human Resources department and the department manager is Mrs Fields\ +INFO:__main__:Working with Job class\ +INFO:__main__:Creating Job records: just like Person. We use the foreign key\ +INFO:__main__:Reading and print all Job rows (note the value of person)...\ +INFO:__main__:Analyst : 2001-09-22 to 2003-01-30 (495 days) for Andrew\ +INFO:__main__:Senior analyst : 2003-02-01 to 2006-10-22 (1359 days) for Andrew\ +INFO:__main__:Senior business analyst : 2006-10-23 to 2016-12-24 (3715 days) for Andrew\ +INFO:__main__:Admin supervisor : 2012-10-01 to 2014-11-10 (770 days) for Peter\ +INFO:__main__:Admin manager : 2014-11-14 to 2018-01-05 (1148 days) for Peter\ +INFO:__main__:database closes\ +INFO:__main__:Person Andrew had job Analyst with department ID T201\ +INFO:__main__: department name is Information Technology\ +INFO:__main__:Person Andrew had job Senior analyst with department ID B402\ +INFO:__main__: department name is Business Administration\ +INFO:__main__:Person Andrew had job Senior business analyst with department ID B402\ +INFO:__main__: department name is Business Administration\ +INFO:__main__:Person Peter had job Admin supervisor with department ID H504\ +INFO:__main__: department name is Human Resources\ +INFO:__main__:Person Peter had job Admin manager with department ID H504\ +INFO:__main__: department name is Human Resources\ +\ +\ +The following is a list of persons and the departments they've worked for:\ +\ + Andrew \{'Information Technology', 'Business Administration'\}\ + Peter \{'Human Resources'\}\ +\ +\ +Ryans-MacBook-Pro:personjob_modified rdrovdahl$ +\f0\fs24 \cf0 \cb1 \CocoaLigature1 \ +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 +\cf0 \ +\ +\ +\ +\ +\ +\ +\ +\ +} \ No newline at end of file diff --git a/Student/rdrovdahl/lesson07/personjob_modified/personjob assignment.zip b/Student/rdrovdahl/lesson07/personjob_modified/personjob assignment.zip new file mode 100644 index 0000000..3c1db23 Binary files /dev/null and b/Student/rdrovdahl/lesson07/personjob_modified/personjob assignment.zip differ diff --git a/Student/rdrovdahl/lesson07/personjob_modified/personjob.db b/Student/rdrovdahl/lesson07/personjob_modified/personjob.db new file mode 100644 index 0000000..f0466e4 Binary files /dev/null and b/Student/rdrovdahl/lesson07/personjob_modified/personjob.db differ diff --git a/Student/rdrovdahl/lesson07/personjob_modified/personjob_learning.py b/Student/rdrovdahl/lesson07/personjob_modified/personjob_learning.py new file mode 100644 index 0000000..dcf7738 --- /dev/null +++ b/Student/rdrovdahl/lesson07/personjob_modified/personjob_learning.py @@ -0,0 +1,188 @@ +""" + Learning persistence with Peewee and sqlite + delete the database to start over + (but running this program does not require it) +""" +import logging +from personjob_model import * +from datetime import datetime + + +def populate_db(): + """ + add person data to database + """ + + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger(__name__) + + database = SqliteDatabase('personjob.db') + + logger.info('Working with Person class') + logger.info('Note how I use constants and a list of tuples as a simple schema') + logger.info('Normally you probably will have prompted for this from a user') + + PERSON_NAME = 0 + LIVES_IN_TOWN = 1 + NICKNAME = 2 + + people = [ + ('Andrew', 'Sumner', 'Andy'), + ('Peter', 'Seattle', None), + ('Susan', 'Boston', 'Beannie'), + ('Pam', 'Coventry', 'PJ'), + ('Steven', 'Colchester', None), + ] + + logger.info('Creating Person records: iterate through the list of tuples') + logger.info('Prepare to explain any errors with exceptions') + logger.info('and the transaction tells the database to fail on error') + + try: + database.connect() + database.execute_sql('PRAGMA foreign_keys = ON;') + for person in people: + with database.transaction(): + new_person = Person.create( + person_name=person[PERSON_NAME], + lives_in_town=person[LIVES_IN_TOWN], + nickname=person[NICKNAME]) + new_person.save() + logger.info('Database add successful') + + logger.info('Print the Person records we saved...') + for saved_person in Person: + logger.info(f'{saved_person.person_name} lives in {saved_person.lives_in_town} ' +\ + f'and likes to be known as {saved_person.nickname}') + + except Exception as e: + logger.info(f'Error creating = {person[PERSON_NAME]}') + logger.info(e) + logger.info('See how the database protects our data') + + """ + add department data to database + """ + + logger.info('Working with Department class') + + DEPARTMENT_ID = 0 + DEPARTMENT_NAME = 1 + DEPARTMENT_MANAGER = 2 + + departments = [ + ('E101', 'Engineering', 'Mr Sprocket'), + ('T201', 'Information Technology', 'Mr Bits'), + ('B402', 'Business Administration', 'Mrs Pots'), + ('H504', 'Human Resources', 'Mrs Fields'), + ] + + logger.info('Creating Department records: iterate through the list of tuples') + logger.info('Prepare to explain any errors with exceptions') + logger.info('and the transaction tells the database to fail on error') + + try: + for department in departments: + with database.transaction(): + new_department = Department.create( + dept_id=department[DEPARTMENT_ID], + dept_name=department[DEPARTMENT_NAME], + dept_manager=department[DEPARTMENT_MANAGER]) + new_department.save() + logger.info('Database add successful') + + logger.info('Print the Department records we saved...') + for saved_department in Department: + logger.info(f'{saved_department.dept_id} is the ID for the {saved_department.dept_name} department ' +\ + f'and the department manager is {saved_department.dept_manager}') + + except Exception as e: + logger.info(f'Error creating = {department[DEPARTMENT_NAME]}') + logger.info(e) + logger.info('See how the database protects our data') + + """ + add job data to database + """ + + logger.info('Working with Job class') + logger.info('Creating Job records: just like Person. We use the foreign key') + + JOB_NAME = 0 + START_DATE = 1 + END_DATE = 2 + SALARY = 3 + PERSON_EMPLOYED = 4 + DEPT_ID = 5 + + jobs = [ + ('Analyst', '2001-09-22', '2003-01-30', 65500, 'Andrew', 'T201'), + ('Senior analyst', '2003-02-01', '2006-10-22', 70000, 'Andrew', 'B402'), + ('Senior business analyst', '2006-10-23', '2016-12-24', 80000, 'Andrew', 'B402'), + ('Admin supervisor', '2012-10-01', '2014-11-10', 45900, 'Peter', 'H504'), + ('Admin manager', '2014-11-14', '2018-01-05', 45900, 'Peter', 'H504') + ] + + try: + for job in jobs: + # compute the job duration in number of days + date_format = '%Y%m%d' + a = job[START_DATE].replace('-', '') + b = job[END_DATE].replace('-', '') + d0 = datetime.strptime(a, date_format) + d1 = datetime.strptime(b, date_format) + JD = (d1 - d0).days + + with database.transaction(): + new_job = Job.create( + job_name=job[JOB_NAME], + start_date=job[START_DATE], + end_date=job[END_DATE], + salary=job[SALARY], + person_employed=job[PERSON_EMPLOYED], + dept_id=job[DEPT_ID], + job_duration=JD) + new_job.save() + + logger.info('Reading and print all Job rows (note the value of person)...') + for job in Job: + logger.info(f'{job.job_name} : {job.start_date} to {job.end_date} ({job.job_duration} days) for {job.person_employed}') + + except Exception as e: + logger.info(f'Error creating = {job[JOB_NAME]}') + logger.info(e) + + finally: + logger.info('database closes') + database.close() + + +def print_departments(): + ''' + print out a list of departments a person worked for considering every + job they ever held + ''' + logger = logging.getLogger(__name__) + mydict = {} + # query for all persons who have held jobs + query = (Person + .select(Person, Job) + .join(Job, JOIN.INNER) + ) + for person in query: + logger.info(f'Person {person.person_name} had job {person.job.job_name} with department ID {person.job.dept_id}') + dept = Department.get(Department.dept_id == person.job.dept_id) + logger.info(f' department name is {dept.dept_name}') + try: + mydict[person.person_name].insert(0, dept.dept_name) + except KeyError: + mydict[person.person_name] = [dept.dept_name] + + print('\n\nThe following is a list of persons and the departments they\'ve worked for:\n') + for k, v in mydict.items(): + print(' ', k, set(v)) + print('\n') + +if __name__ == '__main__': + populate_db() + print_departments() diff --git a/Student/rdrovdahl/lesson07/personjob_modified/personjob_model.py b/Student/rdrovdahl/lesson07/personjob_modified/personjob_model.py new file mode 100644 index 0000000..74bcf9f --- /dev/null +++ b/Student/rdrovdahl/lesson07/personjob_modified/personjob_model.py @@ -0,0 +1,65 @@ +""" + Simple database examle with Peewee ORM, sqlite and Python + Here we define the schema and create the database tables +""" + +from peewee import * + +database = SqliteDatabase('personjob.db') +database.connect() +database.execute_sql('PRAGMA foreign_keys = ON;') + +class BaseModel(Model): + class Meta: + database = database + + +class Person(BaseModel): + """ + This class defines Person, which maintains details of someone + for whom we want to research career to date. + """ + person_name = CharField(primary_key = True, max_length = 30) + lives_in_town = CharField(max_length = 40) + nickname = CharField(max_length = 20, null = True) + +class Department(BaseModel): + """ + This class defines the Departments which all Jobs are associated with. + All Jobs must be associated with a single Department. + """ + dept_id = CharField(primary_key = True, max_length = 4) + dept_name = CharField(max_length = 40) + dept_manager = CharField(max_length = 30) + + +class Job(BaseModel): + """ + This class defines Job, which maintains details of past Jobs + held by a Person. + """ + job_name = CharField(primary_key = True, max_length = 30) + start_date = DateField(formats = 'YYYY-MM-DD') + end_date = DateField(formats = 'YYYY-MM-DD') + salary = DecimalField(max_digits = 7, decimal_places = 2) + person_employed = ForeignKeyField(Person, related_name='was_filled_by', null = False) + job_duration = IntegerField() + dept_id = ForeignKeyField(Department, related_name='department_id', null = False) + +class PersonNumKey(BaseModel): + """ + This class defines Person, which maintains details of someone + for whom we want to research career to date. + + *** I am implemented with a numeric PK that is generated by the system *** + """ + person_name = CharField(max_length = 30) + lives_in_town = CharField(max_length = 40) + nickname = CharField(max_length = 20, null = True) + +database.create_tables([ + Job, + Person, + PersonNumKey, + Department + ]) diff --git a/Student/rdrovdahl/lesson07/personjob_modified/personjob_modified_db_diagram.png b/Student/rdrovdahl/lesson07/personjob_modified/personjob_modified_db_diagram.png new file mode 100644 index 0000000..493e122 Binary files /dev/null and b/Student/rdrovdahl/lesson07/personjob_modified/personjob_modified_db_diagram.png differ diff --git a/Student/rdrovdahl/lesson07/personjob_modified/personjob_modified_db_model.pdf b/Student/rdrovdahl/lesson07/personjob_modified/personjob_modified_db_model.pdf new file mode 100644 index 0000000..97381c9 Binary files /dev/null and b/Student/rdrovdahl/lesson07/personjob_modified/personjob_modified_db_model.pdf differ diff --git a/Student/rdrovdahl/lesson08/no_sql/.gitignore b/Student/rdrovdahl/lesson08/no_sql/.gitignore new file mode 100644 index 0000000..3e3b980 --- /dev/null +++ b/Student/rdrovdahl/lesson08/no_sql/.gitignore @@ -0,0 +1,116 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +.DS_Store + +#secrets +.config/ + +prof/ +.pytest_cache/ +.idea/ + +test_results/ +logs/ + +data/ + diff --git a/Student/rdrovdahl/lesson08/no_sql/README.md b/Student/rdrovdahl/lesson08/no_sql/README.md new file mode 100644 index 0000000..3068ecc --- /dev/null +++ b/Student/rdrovdahl/lesson08/no_sql/README.md @@ -0,0 +1,8 @@ + +[On GitLab now (no more updates here)](https://gitlab.com/uw-pyclass/nosql) + +# nosql + +python certificate nosql example + +All of the Python code is in the src directory. diff --git a/Student/rdrovdahl/lesson08/no_sql/config.ini b/Student/rdrovdahl/lesson08/no_sql/config.ini new file mode 100644 index 0000000..dd8cde7 --- /dev/null +++ b/Student/rdrovdahl/lesson08/no_sql/config.ini @@ -0,0 +1,13 @@ +[mongodb_cloud] +user = admin +pw = mongodbpass +connection = mongodb://{user}:{pw}@cluster0-shard-00-00-rdssm.mongodb.net:27017,cluster0-shard-00-01-rdssm.mongodb.net:27017,cluster0-shard-00-02-rdssm.mongodb.net:27017/test?ssl=true&replicaSet=Cluster0-shard-0&authSource=admin&retryWrites=true + +[redis_cloud] +host = redis-19008.c1.us-west-2-2.ec2.cloud.redislabs.com +port = 19008 +pw = GPazOwRDLCP7dKDoAWpiIL0rcdedhx6I + +[neo4j_cloud] +user = admin +pw = b.aVcDmambd2Z1.lcIBrO9nMpx7Mmxp diff --git a/Student/rdrovdahl/lesson08/no_sql/config.ini.sample b/Student/rdrovdahl/lesson08/no_sql/config.ini.sample new file mode 100644 index 0000000..b069126 --- /dev/null +++ b/Student/rdrovdahl/lesson08/no_sql/config.ini.sample @@ -0,0 +1,13 @@ +[mongodb_cloud] +user = xxxxxxx +pw = xxxxxxxx +connection = + +[redis_cloud] +host = redis-xxxxx.c9.us-east-1-2.ec2.cloud.redislabs.com +port = 18815 +pw = xxxxxxxx + +[neo4j_cloud] +user = xxxx +pw = xxxxxxxxxxxxx diff --git a/Student/rdrovdahl/lesson08/no_sql/src/__init__.py b/Student/rdrovdahl/lesson08/no_sql/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Student/rdrovdahl/lesson08/no_sql/src/learn_data.py b/Student/rdrovdahl/lesson08/no_sql/src/learn_data.py new file mode 100644 index 0000000..b2d37f3 --- /dev/null +++ b/Student/rdrovdahl/lesson08/no_sql/src/learn_data.py @@ -0,0 +1,69 @@ +""" + Data for database demonstrations +""" + + +def get_furniture_data(): + """ + demonstration data + """ + + furniture_data = [ + { + 'product_type': 'couch', + 'product_color': 'red', + 'description': 'Leather low back', + 'monthly_rental_cost': 12.99, + 'in_stock_quantity': 10 + }, + { + 'product_type': 'couch', + 'product_color': 'blue', + 'description': 'Cloth high back', + 'monthly_rental_cost': 9.99, + 'in_stock_quantity': 3 + }, + { + 'product_type': 'coffee table', + 'product_color': 'white', + 'description': 'Plastic', + 'monthly_rental_cost': 2.50, + 'in_stock_quantity': 25 + }, + { + 'product_type': 'couch', + 'product_color': 'red', + 'description': 'Leather high back', + 'monthly_rental_cost': 15.99, + 'in_stock_quantity': 17 + }, + { + 'product_type': 'recliner', + 'product_color': 'blue', + 'description': 'Leather high back', + 'monthly_rental_cost': 19.99, + 'in_stock_quantity': 6 + }, + { + 'product_type': 'chair', + 'product_color': 'black', + 'description': 'Plastic', + 'monthly_rental_cost': 1.00, + 'in_stock_quantity': 45 + }, + { + 'product_type': 'bunk bed', + 'product_color': 'white', + 'description': 'Boys twin size bunk bed', + 'monthly_rental_cost': 5.00, + 'in_stock_quantity': 25 + }, + { + 'product_type': 'rocking chair', + 'product_color': 'red', + 'description': 'Grandmas favorite!', + 'monthly_rental_cost': 2.50, + 'in_stock_quantity': 19 + } + ] + return furniture_data diff --git a/Student/rdrovdahl/lesson08/no_sql/src/learnnosql.py b/Student/rdrovdahl/lesson08/no_sql/src/learnnosql.py new file mode 100644 index 0000000..2cbaa5b --- /dev/null +++ b/Student/rdrovdahl/lesson08/no_sql/src/learnnosql.py @@ -0,0 +1,39 @@ +""" + +Integrated example for nosql databases + +""" + +import learn_data +import mongodb_script +import redis_script +import neo4j_script +import simple_script +import utilities + + +def showoff_databases(): + """ + Here we illustrate basic interaction with nosql databases + """ + + log = utilities.configure_logger('default', '../logs/nosql_dev.log') + + log.info("Mongodb example to use data from Furniture module, so get it") + furniture = learn_data.get_furniture_data() + + mongodb_script.run_example(furniture) + + log.info("Other databases use data embedded in the modules") + + redis_script.run_example() + neo4j_script.run_example() + simple_script.run_example(furniture) + + +if __name__ == '__main__': + """ + orchestrate nosql examples + """ + + showoff_databases() diff --git a/Student/rdrovdahl/lesson08/no_sql/src/login_database.py b/Student/rdrovdahl/lesson08/no_sql/src/login_database.py new file mode 100644 index 0000000..88694ad --- /dev/null +++ b/Student/rdrovdahl/lesson08/no_sql/src/login_database.py @@ -0,0 +1,90 @@ +""" + module that will login to the various demonstration databases consistently +""" + +import configparser +from pathlib import Path +import pymongo +import redis +from neo4j.v1 import GraphDatabase, basic_auth + +import utilities + +log = utilities.configure_logger('default', '../logs/login_databases_dev.log') +config_file = Path(__file__).parent.parent / '.config/config.ini' +config = configparser.ConfigParser() + + +def login_mongodb_cloud(): + """ + connect to mongodb and login + """ + + log.info('Here is where we use the connect to mongodb.') + log.info('Note use of f string to embed the user & password (from the tuple).') + try: + config.read(config_file) + user = config["mongodb_cloud"]["user"] + pw = config["mongodb_cloud"]["pw"] + connection = config["mongodb_cloud"]["connection"] + + except Exception as e: + print(f'error: {e}') + + + # client = pymongo.MongoClient(f'mongodb://{user}:{pw}' + # '@cluster0-shard-00-00-wphqo.mongodb.net:27017,' + # 'cluster0-shard-00-01-wphqo.mongodb.net:27017,' + # 'cluster0-shard-00-02-wphqo.mongodb.net:27017/test' + # '?ssl=true&replicaSet=Cluster0-shard-0&authSource=admin') + + client = pymongo.MongoClient(connection.format(user=user, pw=pw)) + + + + return client + + +def login_redis_cloud(): + """ + connect to redis and login + """ + try: + config.read(config_file) + host = config["redis_cloud"]["host"] + port = config["redis_cloud"]["port"] + pw = config["redis_cloud"]["pw"] + + + except Exception as e: + print(f'error: {e}') + + log.info('Here is where we use the connect to redis.') + + try: + r = redis.StrictRedis(host=host, port=port, password=pw, decode_responses=True) + + except Exception as e: + print(f'error: {e}') + + return r + + +def login_neo4j_cloud(): + """ + connect to neo4j and login + + """ + + log.info('Here is where we use the connect to neo4j.') + log.info('') + + config.read(config_file) + + graphenedb_user = config["neo4j_cloud"]["user"] + graphenedb_pass = config["neo4j_cloud"]["pw"] + graphenedb_url = 'bolt://hobby-opmhmhgpkdehgbkejbochpal.dbs.graphenedb.com:24786' + driver = GraphDatabase.driver(graphenedb_url, + auth=basic_auth(graphenedb_user, graphenedb_pass)) + + return driver diff --git a/Student/rdrovdahl/lesson08/no_sql/src/mongodb_script.py b/Student/rdrovdahl/lesson08/no_sql/src/mongodb_script.py new file mode 100644 index 0000000..a206a12 --- /dev/null +++ b/Student/rdrovdahl/lesson08/no_sql/src/mongodb_script.py @@ -0,0 +1,102 @@ +""" + mongodb example +""" + +import pprint +import login_database +import utilities + +log = utilities.configure_logger('default', '../logs/mongodb_script.log') + + +def run_example(furniture_items): + """ + mongodb data manipulation + """ + + with login_database.login_mongodb_cloud() as client: + log.info('Step 1: We are going to use a database called dev') + log.info('But if it doesnt exist mongodb creates it') + db = client['dev'] + + log.info('And in that database use a collection called furniture') + log.info('If it doesnt exist mongodb creates it') + + furniture = db['furniture'] + + log.info('Step 2: Now we add data from the dictionary above') + furniture.insert_many(furniture_items) + + log.info('Step 3: Find the products that are described as plastic') + query = {'description': 'Plastic'} + results = furniture.find_one(query) + + log.info('Step 4: Print the plastic products') + print('Plastic products') + pprint.pprint(results) + + log.info('Step 4a-1: Print all the couches') + print('All the couches:') + cursor = furniture.find({'product_type': 'couch'}) + for doc in cursor: + pprint.pprint(doc) + + log.info('Step 4a-2: Print all the red items') + print('All the red items:') + cursor = furniture.find({'product_color': 'red'}) + for doc in cursor: + pprint.pprint(doc) + + + log.info('Step 4b: Check for couches that are blue') + query = {"$and": [{"product_type": "couch"}, {"product_color": "blue"}]} + results = furniture.find_one(query) + pprint.pprint(results) + + + log.info('Step 5: Delete all blue couches') + furniture.remove(query) + + log.info('Step 6: Check it is deleted with a query and print') + results = furniture.find_one(query) + print('The blue couch is deleted, print should show none:') + pprint.pprint(results) + + log.info( + 'Step 7: Find multiple documents, iterate though the results and print') + + cursor = furniture.find({'monthly_rental_cost': {'$gte': 15.00}}).sort('monthly_rental_cost', 1) + print('Results of search') + log.info('Notice how we parse out the data from the document') + + for doc in cursor: + print(f"Cost: {doc['monthly_rental_cost']} product name: {doc['product_type']} Stock: {doc['in_stock_quantity']}") + + + log.info('Step 7a: add new documents to the database') + new_furniture_items = [{'product_type': 'dresser', + 'product_color': 'black', + 'description': 'rustic dresser', + 'monthly_rental_cost': 10.00, + 'in_stock_quantity': 2, + 'number_of_drawers': 6}, + {'product_type': 'bed', + 'product_color': 'black', + 'description': 'modern bed', + 'monthly_rental_cost': 25.00, + 'in_stock_quantity': 5, + 'size': 'king'}] + furniture.insert_many(new_furniture_items) + + log.info('Step 7b: Validate new documents made it into the database') + print('query and print new items from database') + query = {"$and": [{"product_type": "dresser"}, {"product_color": "black"}]} + results = furniture.find_one(query) + pprint.pprint(results) + + query = {"$and": [{"product_type": "bed"}, {"product_color": "black"}]} + results = furniture.find_one(query) + pprint.pprint(results) + + log.info('Step 8: Delete the collection so we can start over') + db.drop_collection('furniture') diff --git a/Student/rdrovdahl/lesson08/no_sql/src/neo4j_script.py b/Student/rdrovdahl/lesson08/no_sql/src/neo4j_script.py new file mode 100644 index 0000000..99f21ed --- /dev/null +++ b/Student/rdrovdahl/lesson08/no_sql/src/neo4j_script.py @@ -0,0 +1,97 @@ +""" + neo4j example +""" + + +import utilities +import login_database +import utilities + +log = utilities.configure_logger('default', '../logs/neo4j_script.log') + + +def run_example(): + + log.info('Step 1: First, clear the entire database, so we can start over') + log.info("Running clear_all") + + driver = login_database.login_neo4j_cloud() + with driver.session() as session: + session.run("MATCH (n) DETACH DELETE n") + + log.info("Step 2: Add a few people") + + with driver.session() as session: + + log.info('Adding a few Person nodes') + log.info('The cyph language is analagous to sql for neo4j') + for first, last in [('Bob', 'Jones'), + ('Nancy', 'Cooper'), + ('Alice', 'Cooper'), + ('Fred', 'Barnes'), + ('Mary', 'Evans'), + ('Marie', 'Curie'), + ]: + cyph = "CREATE (n:Person {first_name:'%s', last_name: '%s'})" % ( + first, last) + session.run(cyph) + + log.info("Step 3: Get all of people in the DB:") + cyph = """MATCH (p:Person) + RETURN p.first_name as first_name, p.last_name as last_name + """ + result = session.run(cyph) + print("People in database:") + for record in result: + print(record['first_name'], record['last_name']) + + log.info('Step 4: Create some relationships') + log.info("Bob Jones likes Alice Cooper, Fred Barnes and Marie Curie") + + for first, last in [("Alice", "Cooper"), + ("Fred", "Barnes"), + ("Marie", "Curie")]: + cypher = """ + MATCH (p1:Person {first_name:'Bob', last_name:'Jones'}) + CREATE (p1)-[friend:FRIEND]->(p2:Person {first_name:'%s', last_name:'%s'}) + RETURN p1 + """ % (first, last) + session.run(cypher) + + log.info("Step 5: Find all of Bob's friends") + cyph = """ + MATCH (bob {first_name:'Bob', last_name:'Jones'}) + -[:FRIEND]->(bobFriends) + RETURN bobFriends + """ + result = session.run(cyph) + print("Bob's friends are:") + for rec in result: + for friend in rec.values(): + print(friend['first_name'], friend['last_name']) + + log.info("Setting up Marie's friends") + + for first, last in [("Mary", "Evans"), + ("Alice", "Cooper"), + ('Fred', 'Barnes'), + ]: + cypher = """ + MATCH (p1:Person {first_name:'Marie', last_name:'Curie'}) + CREATE (p1)-[friend:FRIEND]->(p2:Person {first_name:'%s', last_name:'%s'}) + RETURN p1 + """ % (first, last) + + session.run(cypher) + + print("Step 6: Find all of Marie's friends?") + cyph = """ + MATCH (marie {first_name:'Marie', last_name:'Curie'}) + -[:FRIEND]->(friends) + RETURN friends + """ + result = session.run(cyph) + print("\nMarie's friends are:") + for rec in result: + for friend in rec.values(): + print(friend['first_name'], friend['last_name']) diff --git a/Student/rdrovdahl/lesson08/no_sql/src/redis_script.py b/Student/rdrovdahl/lesson08/no_sql/src/redis_script.py new file mode 100644 index 0000000..e32e2ef --- /dev/null +++ b/Student/rdrovdahl/lesson08/no_sql/src/redis_script.py @@ -0,0 +1,58 @@ +""" + demonstrate use of Redis +""" + + +import login_database +import utilities + + +def run_example(): + """ + uses non-presistent Redis only (as a cache) + + """ + + log = utilities.configure_logger('default', '../logs/redis_script.log') + + try: + log.info('Step 1: connect to Redis') + r = login_database.login_redis_cloud() + log.info('Step 2: cache some data in Redis') + r.set('andy', 'andy@somewhere.com') + + log.info('Step 2: now I can read it') + email = r.get('andy') + log.info('But I must know the key') + log.info(f'The results of r.get: {email}') + + log.info('Step 3: cache more data in Redis') + r.set('pam', 'pam@anywhere.com') + r.set('fred', 'fred@fearless.com') + + log.info('Step 4: delete from cache') + r.delete('andy') + log.info(f'r.delete means andy is now: {email}') + + log.info( + 'Step 6: Redis can maintain a unique ID or count very efficiently') + r.set('user_count', 21) + r.incr('user_count') + r.incr('user_count') + r.decr('user_count') + result = r.get('user_count') + log.info('I could use this to generate unique ids') + log.info(f'Redis says 21+1+1-1={result}') + + log.info('Step 7: richer data for a SKU') + r.rpush('186675', 'chair') + r.rpush('186675', 'red') + r.rpush('186675', 'leather') + r.rpush('186675', '5.99') + + log.info('Step 8: pull some data from the structure') + cover_type = r.lindex('186675', 2) + log.info(f'Type of cover = {cover_type}') + + except Exception as e: + print(f'Redis error: {e}') diff --git a/Student/rdrovdahl/lesson08/no_sql/src/simple_script.py b/Student/rdrovdahl/lesson08/no_sql/src/simple_script.py new file mode 100644 index 0000000..163f679 --- /dev/null +++ b/Student/rdrovdahl/lesson08/no_sql/src/simple_script.py @@ -0,0 +1,113 @@ +""" +pickle etc +""" + +import pickle +import shelve +import csv +import json + +import pprint +import utilities + +log = utilities.configure_logger('default', '../logs/mongodb_script.log') + + +def run_example(furniture_items): + """ + various persistence and serialization scenarios + + """ + + def run_pickle(): + """ + Write and read with pickle + """ + log.info("\n\n====") + log.info('Step 1: Demonstrate persistence with pickle') + log.info('Write a pickle file with the furniture data') + + pickle.dump(furniture_items, open('../data/data.pkl', 'wb')) + + log.info('Step 2: Now read it back from the pickle file') + read_data = pickle.load(open('../data/data.pkl', 'rb')) + log.info('Step 3: Show that the write and read were successful') + assert read_data == furniture_items + log.info("and print the data") + pprint.pprint(read_data) + + def run_shelve(): + """ + write and read with shelve + + """ + log.info("\n\n====") + log.info("Step 4: Demonstrate working with shelve") + shelf_file = shelve.open('../data/shelve.dat') + log.info("Step 5: store data at key") + shelf_file['key'] = furniture_items + + log.info("Step 6: Now retrieve a COPY of data at key") + read_items = shelf_file['key'] + + log.info("Check it worked") + assert read_items == furniture_items + + log.info("And now print the copy") + pprint.pprint(read_items) + + log.info("Step 7: delete data stored at key to cleanup and close") + del shelf_file['key'] + shelf_file.close() + + def run_csv(): + """ + write and read a csv + """ + log.info("\n\n====") + peopledata = [ + ('John', 'second guitar', 117.45), + ('Paul', 'bass', 22.01), + ('George', 'lead guitar', 45.99), + ('Ringo', 'drume', 77.0), + ('Roger', 'vocals', 12.5), + ('Keith', 'drums', 6.25), + ('Pete', 'guitar', 0.1), + ('John', 'bass', 89.71) + ] + log.info("Step 8: Write csv file") + with open('../data/rockstars.csv', 'w') as people: + peoplewriter = csv.writer(people) + peoplewriter.writerow(peopledata) + + log.info("Step 9: Read csv file back") + with open('../data/rockstars.csv', 'r') as people: + people_reader = csv.reader(people, delimiter=',', quotechar='"') + for row in people_reader: + pprint.pprint(row) + + def run_json(): + log.info("\n\n====") + log.info("Step 10: Look at working with json data") + furniture = [{'product': 'Red couch','description': 'Leather low back'}, + {'product': 'Blue couch','description': 'Cloth high back'}, + {'product': 'Coffee table','description': 'Plastic'}, + {'product': 'Red couch','description': 'Leather high back'}] + + log.info("Step 11: Return json string from an object") + furniture_string = json.dumps(furniture) + + log.info("Step 12: Print the json") + pprint.pprint(furniture_string) + + log.info("Step 13: Returns an object from a json string representation") + furniture_object = json.loads(furniture_string) + log.info("Step 14: print the string") + pprint.pprint(furniture_object) + + run_pickle() + run_shelve() + run_csv() + run_json() + + return diff --git a/Student/rdrovdahl/lesson08/no_sql/src/utilities.py b/Student/rdrovdahl/lesson08/no_sql/src/utilities.py new file mode 100644 index 0000000..15b1079 --- /dev/null +++ b/Student/rdrovdahl/lesson08/no_sql/src/utilities.py @@ -0,0 +1,43 @@ +""" +enable easy and controllable logging +""" + +import logging +import logging.config + + +def configure_logger(name, log_path): + """ + generic logger + """ + logging.config.dictConfig({ + 'version': 1, + 'formatters': { + 'default': {'format': '%(asctime)s - %(levelname)s - %(message)s', 'datefmt': '%Y-%m-%d %H:%M:%S'} + }, + 'handlers': { + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'default', + 'stream': 'ext://sys.stdout' + }, + 'file': { + 'level': 'DEBUG', + 'class': 'logging.handlers.RotatingFileHandler', + 'formatter': 'default', + 'filename': log_path, + 'maxBytes': 1024, + 'backupCount': 3 + } + }, + 'loggers': { + 'default': { + 'level': 'DEBUG', + 'handlers': ['console', 'file'] + } + }, + 'disable_existing_loggers': False + }) + return logging.getLogger(name) + diff --git a/Student/rdrovdahl/lesson09/__pycache__/integrate.cpython-36.pyc b/Student/rdrovdahl/lesson09/__pycache__/integrate.cpython-36.pyc new file mode 100644 index 0000000..bf6a7ff Binary files /dev/null and b/Student/rdrovdahl/lesson09/__pycache__/integrate.cpython-36.pyc differ diff --git a/Student/rdrovdahl/lesson09/async/get_news_async.py b/Student/rdrovdahl/lesson09/async/get_news_async.py new file mode 100644 index 0000000..8641e0a --- /dev/null +++ b/Student/rdrovdahl/lesson09/async/get_news_async.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python + +""" +An Asynchronous version of the script to see how much a given word is +mentioned in the news today + +Uses data from the NewsAPI: + +https://newsapi.org +""" + +import time +import asyncio +import aiohttp + +NEWS_API_KEY = "84d0483394c44f288965d7b366e54a74" + +WORD = "war" +base_url = 'https://newsapi.org/v1/' + + +# This has to run first, so doesn't really need async +# but why use two requests libraries ? +async def get_sources(sources): + """ + Get all the english language sources of news + + 'https://newsapi.org/v1/sources?language=en' + """ + url = base_url + "sources" + params = {"language": "en", "apiKey": NEWS_API_KEY} + async with aiohttp.ClientSession() as session: + async with session.get(url, ssl=False, params=params) as resp: + data = await resp.json() + print("Got the sources") + sources.extend([src['id'].strip() for src in data['sources']]) + + +async def get_articles(source): + """ + download the info for all the articles + """ + url = base_url + "articles" + params = {"source": source, + "apiKey": NEWS_API_KEY, + "sortBy": "top" + } + print("requesting:", source) + async with aiohttp.ClientSession() as session: + async with session.get(url, ssl=False, params=params) as resp: + if resp.status != 200: # aiohttpp has "status" + print(f'something went wrong with: {source}') + await asyncio.sleep(0) # releases control the the mainloop + return + # awaits response rather than waiting on response in the requests version of this + print("got the articles from {}".format(source)) + data = await resp.json() + # the url to the article itself is in data['articles'][i]['url'] + titles.extend([(str(art['title']) + str(art['description'])) + for art in data['articles']]) + + +def count_word(word, titles): + word = word.lower() + count = 0 + for title in titles: + if word in title.lower(): + count += 1 + return count + + +start = time.time() + +# start up a loop: +loop = asyncio.get_event_loop() + +# create the objects to hold the data +sources = [] +titles = [] + +# get the sources -- this is essentially synchronous +loop.run_until_complete(get_sources(sources)) + +# run the loop for the articles +jobs = asyncio.gather(*(get_articles(source) for source in sources)) +loop.run_until_complete(jobs) +loop.close() + +art_count = len(titles) +word_count = count_word(WORD, titles) + +print(f'found {WORD}, {word_count} times in {art_count} articles') +print(f'Process took {(time.time() - start):.0f} sec.') diff --git a/Student/rdrovdahl/lesson09/async/get_news_sync.py b/Student/rdrovdahl/lesson09/async/get_news_sync.py new file mode 100644 index 0000000..82a7b13 --- /dev/null +++ b/Student/rdrovdahl/lesson09/async/get_news_sync.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python + +""" +Regular synchronous script to see how much a given word is mentioned in the +news today + +Took about 21 seconds for me. + +Uses data from the NewsAPI: + +https://newsapi.org + +NOTE: you need to register with the web site to get a KEY. +""" +import time +import requests + +WORD = "trump" + +NEWS_API_KEY = "84d0483394c44f288965d7b366e54a74" + +base_url = 'https://newsapi.org/v1/' + + +def get_sources(): + """ + Get all the english language sources of news + + 'https://newsapi.org/v1/sources?language=en' + """ + url = base_url + "sources" + params = {"language": "en"} + resp = requests.get(url, params=params) + data = resp.json() + sources = [src['id'].strip() for src in data['sources']] + print("all the sources") + print(sources) + return sources + + +def get_articles(source): + """ + https://newsapi.org/v1/articles?source=associated-press&sortBy=top&apiKey=1fabc23bb9bc485ca59b3966cbd6ea26 + """ + url = base_url + "articles" + params = {"source": source, + "apiKey": NEWS_API_KEY, + # "sortBy": "latest", # some sources don't support latest + "sortBy": "top", + # "sortBy": "popular", + } + print("requesting:", source) + resp = requests.get(url, params=params) + if resp.status_code != 200: # aiohttpp has "status" + print("something went wrong with {}".format(source)) + print(resp) + print(resp.text) + return [] + data = resp.json() + # the url to the article itself is in data['articles'][i]['url'] + titles = [str(art['title']) + str(art['description']) + for art in data['articles']] + return titles + + +def count_word(word, titles): + word = word.lower() + count = 0 + for title in titles: + if word in title.lower(): + count += 1 + return count + + +start = time.time() +sources = get_sources() + +art_count = 0 +word_count = 0 +for source in sources: + titles = get_articles(source) + art_count += len(titles) + word_count += count_word('trump', titles) + +print(WORD, "found {} times in {} articles".format(word_count, art_count)) +print("Process took {:.0f} seconds".format(time.time() - start)) diff --git a/Student/rdrovdahl/lesson09/async/just_coroutines.py b/Student/rdrovdahl/lesson09/async/just_coroutines.py new file mode 100644 index 0000000..68d4723 --- /dev/null +++ b/Student/rdrovdahl/lesson09/async/just_coroutines.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 + +''' +Some experimental code working with coroutines by themselves, outside of an +async framework or event loop. +''' + + +# note - using the async keyword makes this function a coroutine +async def corout(): + print('running corout') + return 'something returned' + +# Note that the returned value gets tacked on to the StopIteration + +async def corout2(): + print('running corout2') + await corout() + + +# You can also create coroutines by using the coroutine decorator from the +# types module + +from types import coroutine + +''' +applying the coroutine decorator makes a generator a coroutine, and thus an async... +''' + +@coroutine +def do_nothing(): + ''' + Here is one that does absolutely nothing + but it can be awaited + ''' + yield 'something from do_nothing()' + + +# Another example of a coroutine that awaits on the first one + +async def do_a_few_things(num=3): + # a loop for multiple things + for i in range(num): + print(f'in the loop for the {i}th time') + res = await do_nothing() + print('res is:', res) + +daft = do_a_few_things() +# daft.send(None) + +while True: + try: + daft.send(None) + except StopIteration: + print('The awaitable is complete') + break + + +''' +now we have what we need to make something that might look a bit like a task +loop +''' +print('\n\n*************\n\n') + +@coroutine +def nothing(): + yield 'yielded from nothing' + return ('returned from nothing') + +@coroutine +def count(num): + for i in range(num): + yield f'count: {i} from the inner loop' + return 'returned from count' + +async def do_a_few_things(num=3, name='no_name'): + for i in range(num): + print(f'in the '{name}' middle loop for the {i}th time') + from_await = await count(i + 2) + print('value returned from await:', from_await) + +# we're going to create a class to make a task loop +class TaskLoop(): + def __init__(self): + # list to hold the tasks + self.tasks = [] + + def add_task(self, task): + # add a task to the loop task must be a coroutine + self.tasks.append(task) + + def run_all(self): + # this is where the task loop runs + # keep a loop going until all the tasks are gone + i = 0 + while self.tasks: + i += 1 + print(f'\nOuter loop count: {i}') + # pop a task off the end + task = self.tasks.pop() + # run that task + try: + res = task.send(None) + print('result of send:', res) + #put it back on the beginning of the task list + self.tasks.insert(0, task) + except StopIteration: + # this will be raised if it is done + # so we don't put it back on the task list + print('The awaitable is complete') + +print('\n\n*** Running the Loop class\n') + +# to use it we create a task loop object and add tasks to it +loop = TaskLoop() +loop.add_task(do_a_few_things(2, 'first task')) +loop.add_task(do_a_few_things(4, 'second task')) +loop.add_task(do_a_few_things(3, 'third task')) + +# and then call run_all +loop.run_all() diff --git a/Student/rdrovdahl/lesson09/jupyter notebook activity/Archive.zip b/Student/rdrovdahl/lesson09/jupyter notebook activity/Archive.zip new file mode 100644 index 0000000..bf73dc6 Binary files /dev/null and b/Student/rdrovdahl/lesson09/jupyter notebook activity/Archive.zip differ diff --git a/Student/rdrovdahl/lesson09/jupyter notebook activity/Concurrency and Async Programming.ipynb b/Student/rdrovdahl/lesson09/jupyter notebook activity/Concurrency and Async Programming.ipynb new file mode 100644 index 0000000..4b08975 --- /dev/null +++ b/Student/rdrovdahl/lesson09/jupyter notebook activity/Concurrency and Async Programming.ipynb @@ -0,0 +1,407 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Concurrency & Async Programming\n", + "\n", + "### working with:\n", + " multi threading\n", + " multi processing\n", + " async" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Parallelism - processing multiple things at the same time. True parallelism requires multiple processors (or cores)\n", + "\n", + "Concurrency - handling multiple things at the same time which may or may not be actually running in the processor at the same time (such as network requests)\n", + "\n", + "Parallelism requires concurrency but concurrency need not be in parallel" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Concurrency: Having differenct code running at the same time\n", + "### Asynchrony: The occurrency of events indipendent of the main program flow and ways to deal with such events\n", + "\n", + "## Types of Concurrency\n", + "#### Multithreading\n", + " Multiple code paths sharing memory - one Python interpreter, one set of Python objects\n", + "#### Multiprocessing\n", + " Multiple code paths with separate memory space - completely separate Python interpreter\n", + "#### Asyncronous programming\n", + " Multiple 'jobs' run at 'arbitrary' times - but usually in one thread, i.e. only one code path, one interpreter" + ] + }, + { + "attachments": { + "image.png": { + "image/png": "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" + } + }, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Other Options:\n", + "### Micro-threads implemented via coroutines and a scheduler\n", + " A coroutine is a generalization of a subroutine which allows multiple entry poiints for suspending and resuming execution.\n", + " Coroutine based solutions follow a cooperative multitasking model\n", + "\n", + "## Threads:\n", + "Threads are lightweight processes run in the address space of an OS process. Threads can not gain the performance advantage of multiple processors due to the Global Interpreter Lock (GIL). However the GIL is released during IO allowing IO bound processes to benefit from threading.\n", + "\n", + "## Processes:\n", + "A process contains all the instructions and data required to execute independently so processes do NOT share data. Multiple processes use to speed up CPU bound operations. \n", + "\n", + "Communication between processes can be achieved via:\n", + " - multiprocessing.Queue\n", + " - multiprocessing.Pipe\n", + " - regular IPC(inter-process communication) must be pickleable\n", + "\n", + "![image.png](attachment:image.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Parallel execution example" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def f(x):\n", + " return x**2\n", + "\n", + "def integrate(f, a, b, N):\n", + " s = 0\n", + " dx = (b-a)/N\n", + " for i in xrange(N):\n", + " s += f(a+i*dx)\n", + " return s * dx" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### we can do better than this\n", + "- break down the problem into parallelizable chunks, then add the results together" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def f(x):\n", + " return x**2\n", + "\n", + "\n", + "def integrate(f, a, b, N):\n", + " s = 0\n", + " dx = (b - a) / N\n", + " for i in range(N):\n", + " s += f(a + i * dx)\n", + " return s * dx\n", + "\n", + "\n", + "def integrate_f_with_functional_tools(f, a, b, N):\n", + " dx = (b - a) / N\n", + " return sum(map(f, ((a + y * dx) for y in range(N)))) * dx\n", + "\n", + "\n", + "# imported here so the rest of the code can run without it\n", + "import numpy as np\n", + "\n", + "\n", + "def integrate_numpy(f, a, b, N):\n", + " \"\"\"\n", + " numpy can be used to \"vectorize\" the problem\n", + "\n", + " f must be \"numpy comaptible\"\n", + "\n", + " \"\"\"\n", + " dx = (b - a) / N\n", + " i = np.arange(N)\n", + " s = np.sum(f(a + (i * dx)))\n", + " return s * dx\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example of how to start a thread" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "import threading\n", + "import time\n", + "\n", + "def func():\n", + " for i in range(5):\n", + " print(\"hello from thread %s\" % threading.current_thread().name)\n", + " time.sleep(1)\n", + "\n", + "threads = []\n", + "for i in range(3):\n", + " thread = threading.Thread(target=func, args=())\n", + " thread.start()\n", + " threads.append(thread)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example of how to use threading in a class" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import threading\n", + "\n", + "class MyThread(threading.Thread):\n", + "\n", + " def run(self):\n", + " print(\"hello from %s\" % threading.current_thread().name)\n", + "\n", + "thread = MyThread()\n", + "thread.start()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example of how to use Mutex locks to prevent race condition" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "x = 0\n", + "x_lock = threading.Lock()\n", + "\n", + "# Example critical section\n", + "with x_lock:\n", + " # statements using x" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example of how to use RLock to prevent race condition" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import threading\n", + "import time\n", + "\n", + "lock = threading.Lock()\n", + "\n", + "def f():\n", + " lock.acquire()\n", + " print(\"%s got lock\" % threading.current_thread().name)\n", + " time.sleep(1)\n", + " lock.release()\n", + "\n", + "threading.Thread(target=f).start()\n", + "threading.Thread(target=f).start()\n", + "threading.Thread(target=f).start()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example of Nonblocking locking" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# .acquire() will return True if it successfully acquires a lock\n", + "# Its first argument is a boolean which specifies whether a lock should block or not. The default is True\n", + "\n", + "import threading\n", + "lock = threading.Lock()\n", + "lock.acquire()\n", + "if not lock.acquire(False):\n", + " print(\"couldn't get lock\")\n", + "lock.release()\n", + "if lock.acquire(False):\n", + " print(\"got lock\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Reentrant Lock - threading.RLock\n", + "Useful for recursive algorithms\n", + "A reentrant lock can be acquired multiple times by the same thread\n", + "Lock.release() must be called the same number of times as Lock.acquire() by a thread" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### The Queue module allows a thread safe way of storing results from multiple threads of execution.\n", + "### Example:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from queue import Queue\n", + "q = Queue(maxsize=10)\n", + "q.put(37337)\n", + "block = True\n", + "timeout = 2\n", + "print(q.get(block, timeout))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Common use: producer/consumer of patterns" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from queue import Queue\n", + "data_q = Queue()\n", + "\n", + "Producer thread:\n", + "for item in produce_items():\n", + " data_q.put(item)\n", + "\n", + "Consumer thread:\n", + "while True:\n", + " item = q.get()\n", + " consume_item(item)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Threading example with a queue" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#!/usr/bin/env python\n", + "\n", + "import threading\n", + "import queue\n", + "\n", + "# from integrate.integrate import integrate, f\n", + "from integrate import f, integrate_numpy as integrate\n", + "from decorators import timer\n", + "\n", + "\n", + "@timer\n", + "def threading_integrate(f, a, b, N, thread_count=2):\n", + " \"\"\"break work into N chunks\"\"\"\n", + " N_chunk = int(float(N) / thread_count)\n", + " dx = float(b - a) / thread_count\n", + "\n", + " results = queue.Queue()\n", + "\n", + " def worker(*args):\n", + " results.put(integrate(*args))\n", + "\n", + " for i in range(thread_count):\n", + " x0 = dx * i\n", + " x1 = x0 + dx\n", + " thread = threading.Thread(target=worker, args=(f, x0, x1, N_chunk))\n", + " thread.start()\n", + " print(\"Thread %s started\" % thread.name)\n", + "\n", + " return sum((results.get() for i in range(thread_count)))\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + "\n", + " # parameters of the integration\n", + " a = 0.0\n", + " b = 10.0\n", + " N = 10**8\n", + " thread_count = 8\n", + "\n", + " print(\"Numerical solution with N=%(N)d : %(x)f\" %\n", + " {'N': N, 'x': threading_integrate(f, a, b, N, thread_count=thread_count)})" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.4" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/Student/rdrovdahl/lesson09/jupyter notebook activity/async coroutines.ipynb b/Student/rdrovdahl/lesson09/jupyter notebook activity/async coroutines.ipynb new file mode 100644 index 0000000..8e31884 --- /dev/null +++ b/Student/rdrovdahl/lesson09/jupyter notebook activity/async coroutines.ipynb @@ -0,0 +1,444 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# define a couple of async coroutines\n", + "\n", + "async def corout():\n", + " print('running corout')\n", + " return 'something returned'\n", + "\n", + "# Note that the returned value gets tacked on to the StopIteration\n", + "\n", + "async def corout2():\n", + " print('running corout2')\n", + " await corout()" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "cr = corout()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "coroutine" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# checkout the object type...\n", + "type(cr)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "running corout\n" + ] + }, + { + "ename": "StopIteration", + "evalue": "something returned", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mStopIteration\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;31m# run the code in the coroutine\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0mcr\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;31mStopIteration\u001b[0m: something returned" + ] + } + ], + "source": [ + "# run the code in the coroutine\n", + "cr.send(None)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "running corout2\n", + "running corout\n" + ] + }, + { + "ename": "StopIteration", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mStopIteration\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;31m# run the second coroutine\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[0mcr2\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mcorout2\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m \u001b[0mcr2\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;31mStopIteration\u001b[0m: " + ] + } + ], + "source": [ + "# run the second coroutine\n", + "cr2 = corout2()\n", + "cr2.send(None)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "# another method to create coroutines is to use the coroutine decorator form the types module\n", + "\n", + "from types import coroutine\n", + "\n", + "@coroutine\n", + "def do_nothing():\n", + " '''\n", + " Here is one that does absolutely nothing\n", + " but it can be awaited\n", + " '''\n", + " yield\n", + " \n", + "dn = do_nothing()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "# run the coroutine\n", + "dn.send(None)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "ename": "StopIteration", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mStopIteration\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;31m# nothing appeared to happen above because the do_nothing coroutine doesn't rield anything\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[0;31m# run it again and you'll see the stop iteration is returned however\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m \u001b[0mdn\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;31mStopIteration\u001b[0m: " + ] + } + ], + "source": [ + "# nothing appeared to happen above because the do_nothing coroutine doesn't rield anything\n", + "# run it again and you'll see the stop iteration is returned however\n", + "dn.send(None)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "in the loop for the 0th time\n" + ] + }, + { + "data": { + "text/plain": [ + "'something from do_nothing()'" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# another example of a coroutine that awaits on the first one\n", + "from types import coroutine\n", + "\n", + "'''\n", + "applying the coroutine decorator makes a generator a coroutine, and thus an async...\n", + "'''\n", + "\n", + "@coroutine\n", + "def do_nothing():\n", + " '''\n", + " Here is one that does absolutely nothing\n", + " but it can be awaited\n", + " '''\n", + " yield 'something from do_nothing()'\n", + " return 'return from do_nothing'\n", + "\n", + "async def do_a_few_things(num=3):\n", + " # a loop for multiple things\n", + " for i in range(num):\n", + " print(f'in the loop for the {i}th time')\n", + " res = await do_nothing()\n", + " print('res is:', res)\n", + "\n", + "# create it \n", + "daft = do_a_few_things(5)\n", + "\n", + "# and run it\n", + "daft.send(None)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "res is: return from do_nothing\n", + "in the loop for the 1th time\n" + ] + }, + { + "data": { + "text/plain": [ + "'something from do_nothing()'" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# it's acting as a generator and we need to continue to call it to get to the StopIteration\n", + "daft.send(None)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "res is: return from do_nothing\n", + "in the loop for the 2th time\n" + ] + }, + { + "data": { + "text/plain": [ + "'something from do_nothing()'" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "daft.send(None)" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "res is: return from do_nothing\n", + "in the loop for the 3th time\n" + ] + }, + { + "data": { + "text/plain": [ + "'something from do_nothing()'" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "daft.send(None)" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "res is: return from do_nothing\n", + "in the loop for the 4th time\n" + ] + }, + { + "data": { + "text/plain": [ + "'something from do_nothing()'" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "daft.send(None)" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "res is: return from do_nothing\n" + ] + }, + { + "ename": "StopIteration", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mStopIteration\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;31m# this one will get the StopIteration\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0mdaft\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;31mStopIteration\u001b[0m: " + ] + } + ], + "source": [ + "# this one will get the StopIteration\n", + "daft.send(None)" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "in the loop for the 0th time\n", + "res is: None\n", + "in the loop for the 1th time\n", + "res is: None\n", + "in the loop for the 2th time\n", + "res is: None\n", + "in the loop for the 3th time\n", + "res is: None\n", + "in the loop for the 4th time\n", + "res is: None\n", + "The awaitable is complete\n", + "passed out: do_a_few_things result\n" + ] + } + ], + "source": [ + "# now use a while loop to loop through the coroutine until you get the StopIteration\n", + "from types import coroutine\n", + "\n", + "'''\n", + "applying the coroutine decorator makes a generator a coroutine, and thus an async...\n", + "'''\n", + "\n", + "@coroutine\n", + "def do_nothing():\n", + " '''\n", + " Here is one that does absolutely nothing\n", + " but it can be awaited\n", + " '''\n", + " yield 'something from do_nothing()'\n", + "\n", + "async def do_a_few_things(num=3):\n", + " # a loop for multiple things\n", + " for i in range(num):\n", + " print(f'in the loop for the {i}th time')\n", + " res = await do_nothing()\n", + " print('res is:', res)\n", + " return 'do_a_few_things result'\n", + "\n", + "daft = do_a_few_things(5)\n", + "\n", + "while True:\n", + " try:\n", + " daft.send(None)\n", + " except StopIteration as si:\n", + " print('The awaitable is complete')\n", + " print('passed out: ', si)\n", + " break\n", + "\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.4" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/Student/rdrovdahl/lesson09/jupyter notebook activity/async task loop using class pt 2.ipynb b/Student/rdrovdahl/lesson09/jupyter notebook activity/async task loop using class pt 2.ipynb new file mode 100644 index 0000000..679063b --- /dev/null +++ b/Student/rdrovdahl/lesson09/jupyter notebook activity/async task loop using class pt 2.ipynb @@ -0,0 +1,2558 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "*** Running the Loop with fibbonacci numbers\n", + "\n", + "\n", + "Outer loop count: 1\n", + "returned from send: 0.0 seconds have passed\n", + "\n", + "Outer loop count: 2\n", + "returned from send: 1.1920928955078125e-06 seconds have passed\n", + "\n", + "Outer loop count: 3\n", + "returned from send: 0.0 seconds have passed\n", + "\n", + "Outer loop count: 4\n", + "returned from send: 0.0 seconds have passed\n", + "\n", + "Outer loop count: 5\n", + "returned from send: 1.1920928955078125e-06 seconds have passed\n", + "\n", + "Outer loop count: 6\n", + "returned from send: 9.5367431640625e-07 seconds have passed\n", + "\n", + "Outer loop count: 7\n", + "returned from send: 0.0 seconds have passed\n", + "\n", + "Outer loop count: 8\n", + "returned from send: 0.0 seconds have passed\n", + "\n", + "Outer loop count: 9\n", + "returned from send: 9.5367431640625e-07 seconds have passed\n", + "\n", + "Outer loop count: 10\n", + "returned from send: 9.5367431640625e-07 seconds have passed\n", + "\n", + "Outer loop count: 11\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 12\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 13\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 14\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 15\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 16\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 17\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 18\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 19\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 20\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 21\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 22\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 23\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 24\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 25\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 26\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 27\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 28\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 29\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 30\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 31\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 32\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 33\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 34\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 35\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 36\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 37\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 38\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 39\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 40\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 41\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 42\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 43\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 44\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 45\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 46\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 47\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 48\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 49\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 50\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 51\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 52\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 53\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 54\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 55\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 56\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 57\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 58\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 59\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 60\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 61\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 62\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 63\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 64\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 65\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 66\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 67\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 68\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 69\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 70\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 71\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 72\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 73\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 74\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 75\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 76\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 77\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 78\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 79\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 80\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 81\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 82\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 83\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 84\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 85\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 86\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 87\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 88\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 89\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 90\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 91\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 92\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 93\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 94\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 95\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 96\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 97\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 98\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 99\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 100\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 101\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 102\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 103\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 104\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 105\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 106\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 107\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 108\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 109\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 110\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 111\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 112\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 113\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 114\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 115\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 116\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 117\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 118\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 119\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 120\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 121\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 122\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 123\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 124\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 125\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 126\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 127\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 128\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 129\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 130\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 131\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 132\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 133\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 134\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 135\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 136\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 137\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 138\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 139\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 140\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 141\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 142\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 143\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 144\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 145\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 146\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 147\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 148\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 149\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 150\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 151\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 152\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 153\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 154\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 155\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 156\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 157\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 158\n", + "returned from send: yielding in sleep\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Outer loop count: 159\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 160\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 161\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 162\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 163\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 164\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 165\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 166\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 167\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 168\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 169\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 170\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 171\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 172\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 173\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 174\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 175\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 176\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 177\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 178\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 179\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 180\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 181\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 182\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 183\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 184\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 185\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 186\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 187\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 188\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 189\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 190\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 191\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 192\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 193\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 194\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 195\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 196\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 197\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 198\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 199\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 200\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 201\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 202\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 203\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 204\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 205\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 206\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 207\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 208\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 209\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 210\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 211\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 212\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 213\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 214\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 215\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 216\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 217\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 218\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 219\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 220\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 221\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 222\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 223\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 224\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 225\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 226\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 227\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 228\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 229\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 230\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 231\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 232\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 233\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 234\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 235\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 236\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 237\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 238\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 239\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 240\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 241\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 242\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 243\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 244\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 245\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 246\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 247\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 248\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 249\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 250\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 251\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 252\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 253\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 254\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 255\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 256\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 257\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 258\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 259\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 260\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 261\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 262\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 263\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 264\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 265\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 266\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 267\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 268\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 269\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 270\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 271\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 272\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 273\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 274\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 275\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 276\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 277\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 278\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 279\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 280\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 281\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 282\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 283\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 284\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 285\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 286\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 287\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 288\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 289\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 290\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 291\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 292\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 293\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 294\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 295\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 296\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 297\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 298\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 299\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 300\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 301\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 302\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 303\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 304\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 305\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 306\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 307\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 308\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 309\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 310\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 311\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 312\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 313\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 314\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 315\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 316\n", + "returned from send: yielding in sleep\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Outer loop count: 317\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 318\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 319\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 320\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 321\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 322\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 323\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 324\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 325\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 326\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 327\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 328\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 329\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 330\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 331\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 332\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 333\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 334\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 335\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 336\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 337\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 338\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 339\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 340\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 341\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 342\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 343\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 344\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 345\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 346\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 347\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 348\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 349\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 350\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 351\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 352\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 353\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 354\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 355\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 356\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 357\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 358\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 359\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 360\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 361\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 362\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 363\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 364\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 365\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 366\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 367\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 368\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 369\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 370\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 371\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 372\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 373\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 374\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 375\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 376\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 377\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 378\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 379\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 380\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 381\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 382\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 383\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 384\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 385\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 386\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 387\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 388\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 389\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 390\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 391\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 392\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 393\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 394\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 395\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 396\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 397\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 398\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 399\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 400\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 401\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 402\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 403\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 404\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 405\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 406\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 407\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 408\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 409\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 410\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 411\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 412\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 413\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 414\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 415\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 416\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 417\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 418\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 419\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 420\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 421\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 422\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 423\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 424\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 425\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 426\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 427\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 428\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 429\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 430\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 431\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 432\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 433\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 434\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 435\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 436\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 437\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 438\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 439\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 440\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 441\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 442\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 443\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 444\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 445\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 446\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 447\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 448\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 449\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 450\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 451\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 452\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 453\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 454\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 455\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 456\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 457\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 458\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 459\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 460\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 461\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 462\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 463\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 464\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 465\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 466\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 467\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 468\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 469\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 470\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 471\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 472\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 473\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 474\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 475\n", + "returned from send: yielding in sleep\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Outer loop count: 476\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 477\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 478\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 479\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 480\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 481\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 482\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 483\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 484\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 485\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 486\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 487\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 488\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 489\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 490\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 491\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 492\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 493\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 494\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 495\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 496\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 497\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 498\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 499\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 500\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 501\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 502\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 503\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 504\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 505\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 506\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 507\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 508\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 509\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 510\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 511\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 512\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 513\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 514\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 515\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 516\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 517\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 518\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 519\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 520\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 521\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 522\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 523\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 524\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 525\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 526\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 527\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 528\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 529\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 530\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 531\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 532\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 533\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 534\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 535\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 536\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 537\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 538\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 539\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 540\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 541\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 542\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 543\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 544\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 545\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 546\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 547\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 548\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 549\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 550\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 551\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 552\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 553\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 554\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 555\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 556\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 557\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 558\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 559\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 560\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 561\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 562\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 563\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 564\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 565\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 566\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 567\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 568\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 569\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 570\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 571\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 572\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 573\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 574\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 575\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 576\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 577\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 578\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 579\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 580\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 581\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 582\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 583\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 584\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 585\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 586\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 587\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 588\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 589\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 590\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 591\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 592\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 593\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 594\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 595\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 596\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 597\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 598\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 599\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 600\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 601\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 602\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 603\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 604\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 605\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 606\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 607\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 608\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 609\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 610\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 611\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 612\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 613\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 614\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 615\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 616\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 617\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 618\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 619\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 620\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 621\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 622\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 623\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 624\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 625\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 626\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 627\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 628\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 629\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 630\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 631\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 632\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 633\n", + "returned from send: yielding in sleep\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Outer loop count: 634\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 635\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 636\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 637\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 638\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 639\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 640\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 641\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 642\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 643\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 644\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 645\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 646\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 647\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 648\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 649\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 650\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 651\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 652\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 653\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 654\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 655\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 656\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 657\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 658\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 659\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 660\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 661\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 662\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 663\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 664\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 665\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 666\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 667\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 668\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 669\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 670\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 671\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 672\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 673\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 674\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 675\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 676\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 677\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 678\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 679\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 680\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 681\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 682\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 683\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 684\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 685\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 686\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 687\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 688\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 689\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 690\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 691\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 692\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 693\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 694\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 695\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 696\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 697\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 698\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 699\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 700\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 701\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 702\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 703\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 704\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 705\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 706\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 707\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 708\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 709\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 710\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 711\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 712\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 713\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 714\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 715\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 716\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 717\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 718\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 719\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 720\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 721\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 722\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 723\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 724\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 725\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 726\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 727\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 728\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 729\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 730\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 731\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 732\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 733\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 734\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 735\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 736\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 737\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 738\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 739\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 740\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 741\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 742\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 743\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 744\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 745\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 746\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 747\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 748\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 749\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 750\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 751\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 752\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 753\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 754\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 755\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 756\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 757\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 758\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 759\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 760\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 761\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 762\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 763\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 764\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 765\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 766\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 767\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 768\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 769\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 770\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 771\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 772\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 773\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 774\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 775\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 776\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 777\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 778\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 779\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 780\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 781\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 782\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 783\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 784\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 785\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 786\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 787\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 788\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 789\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 790\n", + "returned from send: yielding in sleep\n", + "\n", + "Outer loop count: 791\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Outer loop count: 792\n", + "\n", + "Outer loop count: 793\n", + "\n", + "Outer loop count: 794\n", + "\n", + "Outer loop count: 795\n", + "\n", + "Outer loop count: 796\n", + "\n", + "Outer loop count: 797\n", + "\n", + "Outer loop count: 798\n", + "\n", + "Outer loop count: 799\n", + "\n", + "Outer loop count: 800\n", + "total run time: 1.0152230262756348 seconds\n", + "the results are: [144, 13, 89, 34, 8, 3, 55, 13, 5, 2]\n" + ] + } + ], + "source": [ + "# let's add some tasks that actually take 'time' to complete simulating what would happen if a task were waiting\n", + "# on say a network request or some I/O function to take place/etc.\n", + "# note - we can't use the 'sleep' command because that is blocking instead we will create a sleep coroutine that runs\n", + "# an infinit generator...\n", + "\n", + "import time\n", + "from types import coroutine\n", + "\n", + "@coroutine\n", + "def sleep(secs=0):\n", + " start = time.time()\n", + " # now we need it to yield control\n", + " yield '{} seconds have passed'.format(time.time() - start)\n", + " # and keep yielding it until enough time has passed\n", + " while time.time() - start < secs:\n", + " # now we need it to yield control\n", + " yield 'yielding in sleep'\n", + " return '{} seconds have passed'.format(time.time() - start)\n", + "\n", + "# now we'll create a coroutine that calculates something\n", + "\n", + "async def fib(n):\n", + " # classic fibbonacci number, but with a delay\n", + " if n == 0:\n", + " return 0\n", + " a, b = 0, 1\n", + " for i in range(n - 1):\n", + " a, b = b, a + b\n", + " await sleep(1.0)\n", + " return b \n", + "\n", + "# we're going to create a class to make a task loop\n", + "class TaskLoop():\n", + " def __init__(self):\n", + " # list to hold the tasks\n", + " self.tasks = []\n", + "\n", + " def add_task(self, task):\n", + " # add a task to the loop task must be a coroutine\n", + " self.tasks.append(task)\n", + "\n", + " def run_all(self):\n", + " # this is where the task loop runs\n", + " # list to hold the results\n", + " results = []\n", + " # keep a loop going until all the tasks are gone\n", + " i = 0\n", + " while self.tasks:\n", + " i += 1\n", + " time.sleep(0.001)\n", + " print(f'\\nOuter loop count: {i}')\n", + " # pop a task off the end\n", + " task = self.tasks.pop()\n", + " # run that task\n", + " try:\n", + " res = task.send(None)\n", + " print('returned from send:', res)\n", + " #put it back on the beginning of the task list\n", + " self.tasks.insert(0, task)\n", + " except StopIteration as si:\n", + " # this will be raised if it is done\n", + " # so we don't put it back on the task list\n", + " # whatever is returned is in the exception's args\n", + " results.append(si.args[0])\n", + " return results\n", + " \n", + "print('\\n\\n*** Running the Loop with fibbonacci numbers\\n')\n", + "\n", + "# to use it we create a task loop object and add tasks to it\n", + "loop = TaskLoop()\n", + "loop.add_task(fib(3))\n", + "loop.add_task(fib(5))\n", + "loop.add_task(fib(7))\n", + "loop.add_task(fib(10))\n", + "loop.add_task(fib(4))\n", + "loop.add_task(fib(6))\n", + "loop.add_task(fib(9))\n", + "loop.add_task(fib(11))\n", + "loop.add_task(fib(7))\n", + "loop.add_task(fib(12))\n", + "\n", + "# let's see how long it takes\n", + "start = time.time()\n", + "results = loop.run_all()\n", + "print(f'total run time: {time.time() - start} seconds')\n", + "\n", + "print('the results are:', results)\n", + "\n", + "\n", + "# Note that when this is run with 10 tasks and having a 1.0 second sleep time per task, the result for all tasks\n", + "# to finish is just a little more than 1 second. This demonstrates how that the coroutines are all running in\n", + "# parallel\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.4" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/Student/rdrovdahl/lesson09/jupyter notebook activity/async task loop using class.ipynb b/Student/rdrovdahl/lesson09/jupyter notebook activity/async task loop using class.ipynb new file mode 100644 index 0000000..23f83c7 --- /dev/null +++ b/Student/rdrovdahl/lesson09/jupyter notebook activity/async task loop using class.ipynb @@ -0,0 +1,326 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "*** Running the Loop class\n", + "\n", + "\n", + "Outer loop count: 1\n", + "in the third task loop (middle loop) for the 0th time\n", + "result of send: from the inner loop - count: 0\n", + "\n", + "Outer loop count: 2\n", + "in the second task loop (middle loop) for the 0th time\n", + "result of send: from the inner loop - count: 0\n", + "\n", + "Outer loop count: 3\n", + "in the first task loop (middle loop) for the 0th time\n", + "result of send: from the inner loop - count: 0\n", + "\n", + "Outer loop count: 4\n", + "result of send: from the inner loop - count: 1\n", + "\n", + "Outer loop count: 5\n", + "result of send: from the inner loop - count: 1\n", + "\n", + "Outer loop count: 6\n", + "result of send: from the inner loop - count: 1\n", + "\n", + "Outer loop count: 7\n", + "value returned from await: returned from count\n", + "in the third task loop (middle loop) for the 1th time\n", + "result of send: from the inner loop - count: 0\n", + "\n", + "Outer loop count: 8\n", + "value returned from await: returned from count\n", + "in the second task loop (middle loop) for the 1th time\n", + "result of send: from the inner loop - count: 0\n", + "\n", + "Outer loop count: 9\n", + "value returned from await: returned from count\n", + "in the first task loop (middle loop) for the 1th time\n", + "result of send: from the inner loop - count: 0\n", + "\n", + "Outer loop count: 10\n", + "result of send: from the inner loop - count: 1\n", + "\n", + "Outer loop count: 11\n", + "result of send: from the inner loop - count: 1\n", + "\n", + "Outer loop count: 12\n", + "result of send: from the inner loop - count: 1\n", + "\n", + "Outer loop count: 13\n", + "result of send: from the inner loop - count: 2\n", + "\n", + "Outer loop count: 14\n", + "result of send: from the inner loop - count: 2\n", + "\n", + "Outer loop count: 15\n", + "result of send: from the inner loop - count: 2\n", + "\n", + "Outer loop count: 16\n", + "value returned from await: returned from count\n", + "in the third task loop (middle loop) for the 2th time\n", + "result of send: from the inner loop - count: 0\n", + "\n", + "Outer loop count: 17\n", + "value returned from await: returned from count\n", + "in the second task loop (middle loop) for the 2th time\n", + "result of send: from the inner loop - count: 0\n", + "\n", + "Outer loop count: 18\n", + "value returned from await: returned from count\n", + "The awaitable is complete\n", + "\n", + "Outer loop count: 19\n", + "result of send: from the inner loop - count: 1\n", + "\n", + "Outer loop count: 20\n", + "result of send: from the inner loop - count: 1\n", + "\n", + "Outer loop count: 21\n", + "result of send: from the inner loop - count: 2\n", + "\n", + "Outer loop count: 22\n", + "result of send: from the inner loop - count: 2\n", + "\n", + "Outer loop count: 23\n", + "result of send: from the inner loop - count: 3\n", + "\n", + "Outer loop count: 24\n", + "result of send: from the inner loop - count: 3\n", + "\n", + "Outer loop count: 25\n", + "value returned from await: returned from count\n", + "The awaitable is complete\n", + "\n", + "Outer loop count: 26\n", + "value returned from await: returned from count\n", + "in the second task loop (middle loop) for the 3th time\n", + "result of send: from the inner loop - count: 0\n", + "\n", + "Outer loop count: 27\n", + "result of send: from the inner loop - count: 1\n", + "\n", + "Outer loop count: 28\n", + "result of send: from the inner loop - count: 2\n", + "\n", + "Outer loop count: 29\n", + "result of send: from the inner loop - count: 3\n", + "\n", + "Outer loop count: 30\n", + "result of send: from the inner loop - count: 4\n", + "\n", + "Outer loop count: 31\n", + "value returned from await: returned from count\n", + "The awaitable is complete\n" + ] + } + ], + "source": [ + "# one way to create coroutines is by using the coroutine decorator from the\n", + "# types module\n", + "\n", + "from types import coroutine\n", + "\n", + "@coroutine\n", + "def nothing():\n", + " yield 'yielded from nothing'\n", + " return ('returned from nothing')\n", + "\n", + "@coroutine\n", + "def count(num):\n", + " for i in range(num):\n", + " yield f'from the inner loop - count: {i}'\n", + " return 'returned from count'\n", + "\n", + "async def do_a_few_things(num=3, name='no_name'):\n", + " for i in range(num):\n", + " print(f'in the {name} loop (middle loop) for the {i}th time')\n", + " from_await = await count(i + 2)\n", + " print('value returned from await:', from_await)\n", + "\n", + "# we're going to create a class to make a task loop\n", + "class TaskLoop():\n", + " def __init__(self):\n", + " # list to hold the tasks\n", + " self.tasks = []\n", + "\n", + " def add_task(self, task):\n", + " # add a task to the loop task must be a coroutine\n", + " self.tasks.append(task)\n", + "\n", + " def run_all(self):\n", + " # this is where the task loop runs\n", + " # keep a loop going until all the tasks are gone\n", + " i = 0\n", + " while self.tasks:\n", + " i += 1\n", + " print(f'\\nOuter loop count: {i}')\n", + " # pop a task off the end\n", + " task = self.tasks.pop()\n", + " # run that task\n", + " try:\n", + " res = task.send(None)\n", + " print('result of send:', res)\n", + " #put it back on the beginning of the task list\n", + " self.tasks.insert(0, task)\n", + " except StopIteration:\n", + " # this will be raised if it is done\n", + " # so we don't put it back on the task list\n", + " print('The awaitable is complete')\n", + "\n", + "print('\\n\\n*** Running the Loop class\\n')\n", + "\n", + "# to use it we create a task loop object and add tasks to it\n", + "loop = TaskLoop()\n", + "loop.add_task(do_a_few_things(2, 'first task'))\n", + "loop.add_task(do_a_few_things(4, 'second task'))\n", + "loop.add_task(do_a_few_things(3, 'third task'))\n", + "\n", + "# and then call run_all\n", + "loop.run_all()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "*** Running the Loop with fibbonacci numbers\n", + "\n", + "\n", + "Outer loop count: 1\n", + "total run time: 0.0014290809631347656 seconds\n", + "the results are: [5]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/anaconda3/lib/python3.6/site-packages/ipykernel_launcher.py:69: RuntimeWarning: coroutine 'fib' was never awaited\n" + ] + } + ], + "source": [ + "# let's add some tasks that actually take 'time' to complete simulating what would happen if a task were waiting\n", + "# on say a network request or some I/O function to take place/etc.\n", + "# note - we can't use the 'sleep' command because that is blocking instead we will...\n", + "\n", + "import time\n", + "from types import coroutine\n", + "\n", + "@coroutine\n", + "def sleep(secs=0):\n", + " start = time.time()\n", + " # now we need it to yield control\n", + " yield '{} seconds have passed'.format(time.time() - start)\n", + " # and keep yielding it until enough time has passed\n", + " while time.time() - start < secs:\n", + " # now we need it to yield control\n", + " yield 'yielding in sleep'\n", + " return '{} seconds have passed'.format(time.time() - start)\n", + "\n", + "# now we'll create a coroutine that calculates something\n", + "\n", + "async def fib(n):\n", + " # classic fibbonacci number, but with a delay\n", + " if n == 0:\n", + " return 0\n", + " a, b = 0, 1\n", + " for i in range(n - 1):\n", + " a, b = b, a + b\n", + " # await sleep(0.0)\n", + " return b \n", + "\n", + "# we're going to create a class to make a task loop\n", + "class TaskLoop():\n", + " def __init__(self):\n", + " # list to hold the tasks\n", + " self.tasks = []\n", + "\n", + " def add_task(self, task):\n", + " # add a task to the loop task must be a coroutine\n", + " self.tasks.append(task)\n", + "\n", + " def run_all(self):\n", + " # this is where the task loop runs\n", + " # list to hold the results\n", + " results = []\n", + " # keep a loop going until all the tasks are gone\n", + " i = 0\n", + " while self.tasks:\n", + " i += 1\n", + " time.sleep(0.001)\n", + " print(f'\\nOuter loop count: {i}')\n", + " # pop a task off the end\n", + " task = self.tasks.pop()\n", + " # run that task\n", + " try:\n", + " res = task.send(None)\n", + " print('result of send:', res)\n", + " #put it back on the beginning of the task list\n", + " self.tasks.insert(0, task)\n", + " except StopIteration as si:\n", + " # this will be raised if it is done\n", + " # so we don't put it back on the task list\n", + " # whatever is returned is in the exception's args\n", + " results.append(si.args[0])\n", + " return results\n", + " \n", + "print('\\n\\n*** Running the Loop with fibbonacci numbers\\n')\n", + "\n", + "# to use it we create a task loop object and add tasks to it\n", + "loop = TaskLoop()\n", + "loop.add_task(fib(10))\n", + "loop.add_task(fib(5))\n", + "# loop.add_task(fib(7))\n", + "\n", + "# let's see how long it takes\n", + "start = time.time()\n", + "results = loop.run_all()\n", + "print(f'total run time: {time.time() - start} seconds')\n", + "\n", + "print('the results are:', results)\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.4" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/Student/rdrovdahl/lesson09/lock_exercise/simple_locks.py b/Student/rdrovdahl/lesson09/lock_exercise/simple_locks.py new file mode 100644 index 0000000..a4456a9 --- /dev/null +++ b/Student/rdrovdahl/lesson09/lock_exercise/simple_locks.py @@ -0,0 +1,14 @@ +import threading +import time + +lock = threading.Lock() + +def f(): + lock.acquire() + print("%s got lock" % threading.current_thread().name) + time.sleep(1) + lock.release() + +threading.Thread(target=f).start() +threading.Thread(target=f).start() +threading.Thread(target=f).start() diff --git a/Student/rdrovdahl/lesson09/lock_exercise/stdout_writer.py b/Student/rdrovdahl/lesson09/lock_exercise/stdout_writer.py new file mode 100644 index 0000000..715ae3f --- /dev/null +++ b/Student/rdrovdahl/lesson09/lock_exercise/stdout_writer.py @@ -0,0 +1,16 @@ +import random +import sys +import threading +import time + + +def write(): + sys.stdout.write("%s writing.." % threading.current_thread().name) + time.sleep(random.random()) + sys.stdout.write("..done\n") + +for i in range(100): + thread = threading.Thread(target=write) + thread.daemon = True # allow ctrl-c to end + thread.start() + time.sleep(.1) diff --git a/Student/rdrovdahl/lesson09/lock_exercise/stdout_writer_semaphore.py b/Student/rdrovdahl/lesson09/lock_exercise/stdout_writer_semaphore.py new file mode 100644 index 0000000..2bc21cd --- /dev/null +++ b/Student/rdrovdahl/lesson09/lock_exercise/stdout_writer_semaphore.py @@ -0,0 +1,19 @@ +import random +import sys +import threading +import time + +lock = threading.Semaphore(2) + +def write(): + lock.acquire() + sys.stdout.write( "%s writing.." % threading.current_thread().name) + time.sleep(random.random()) + sys.stdout.write( "..done\n") + lock.release() + + +while True: + thread = threading.Thread(target=write) + thread.start() + time.sleep(.1) diff --git a/Student/rdrovdahl/lesson09/lock_exercise/stdout_writer_solution.py b/Student/rdrovdahl/lesson09/lock_exercise/stdout_writer_solution.py new file mode 100644 index 0000000..01084a2 --- /dev/null +++ b/Student/rdrovdahl/lesson09/lock_exercise/stdout_writer_solution.py @@ -0,0 +1,27 @@ +import random +import sys +import threading +import time + +lock = threading.Lock() + + +def write(): + lock.acquire() + sys.stdout.write("%s writing.." % threading.current_thread().name) + time.sleep(random.random()) + sys.stdout.write("..done\n") + lock.release() + +threads = [] +for i in range(50): + thread = threading.Thread(target=write) + thread.daemon = True # allow ctrl-c to end + thread.start() + threads.append(thread) + time.sleep(.01) + +# Now join() them all so the program won't terminate early +# required because these are all daemon threads +for thread in threads: + thread.join() diff --git a/Student/rdrovdahl/lesson09/multi_thread&proc/__pycache__/integrate.cpython-36.pyc b/Student/rdrovdahl/lesson09/multi_thread&proc/__pycache__/integrate.cpython-36.pyc new file mode 100644 index 0000000..ccfa7bb Binary files /dev/null and b/Student/rdrovdahl/lesson09/multi_thread&proc/__pycache__/integrate.cpython-36.pyc differ diff --git a/Student/rdrovdahl/lesson09/multi_thread&proc/integrate.py b/Student/rdrovdahl/lesson09/multi_thread&proc/integrate.py new file mode 100644 index 0000000..8d56daf --- /dev/null +++ b/Student/rdrovdahl/lesson09/multi_thread&proc/integrate.py @@ -0,0 +1,32 @@ +def f(x): + return x**2 + + +def integrate(f, a, b, N): + s = 0 + dx = (b - a) / N + for i in range(N): + s += f(a + i * dx) + return s * dx + + +def integrate_f_with_functional_tools(f, a, b, N): + dx = (b - a) / N + return sum(map(f, ((a + y * dx) for y in range(N)))) * dx + + +# imported here so the rest of the code can run without it +import numpy as np + + +def integrate_numpy(f, a, b, N): + """ + numpy can be used to "vectorize" the problem + + f must be "numpy comaptible" + + """ + dx = (b - a) / N + i = np.arange(N) + s = np.sum(f(a + (i * dx))) + return s * dx diff --git a/Student/rdrovdahl/lesson09/multi_thread&proc/integrate_multiproc.py b/Student/rdrovdahl/lesson09/multi_thread&proc/integrate_multiproc.py new file mode 100644 index 0000000..2d877ce --- /dev/null +++ b/Student/rdrovdahl/lesson09/multi_thread&proc/integrate_multiproc.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 + +''' +Program that calls the computation functions in integrate.py and allows +multiprocessing based on the proc_count variable in __main__ +''' + +# import threading +# import queue +import time +from multiprocessing import Queue +import multiprocessing + + +## comment out either integrate or integrate_numpy depending on which API you +## want to test + +from integrate import integrate, f +# from integrate import f, integrate_numpy as integrate + +# from decorators import timer ## this didn't work, ned to investigate. Added a timer to main instead + + + +# @timer +def multi_integrate(f, a, b, N, process_count=2): + """break work into N chunks""" + N_chunk = int(float(N) / process_count) + dx = float(b - a) / process_count + + results = Queue() + + def worker(*args): + results.put(integrate(*args)) + + for i in range(process_count): + x0 = dx * i + x1 = x0 + dx + process = multiprocessing.Process(target=worker, args=(f, x0, x1, N_chunk)) + process.start() + print("process %s started" % process.name) + + return sum((results.get() for i in range(process_count))) + + +if __name__ == "__main__": + + # parameters of the integration + a = 0.0 + b = 10.0 + N = 10**8 + process_count = 16 + + begin = time.time() + print("Numerical solution with N=%(N)d : %(x)f" % + {'N': N, 'x': multi_integrate(f, a, b, N, process_count=process_count)}) + end = time.time() + print('it took: ', end - begin, 'seconds') diff --git a/Student/rdrovdahl/lesson09/multi_thread&proc/integrate_multithreads.py b/Student/rdrovdahl/lesson09/multi_thread&proc/integrate_multithreads.py new file mode 100644 index 0000000..0ac460a --- /dev/null +++ b/Student/rdrovdahl/lesson09/multi_thread&proc/integrate_multithreads.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 + +''' +Program that calls the computation functions in integrate.py and allows +multithreading based on the thread_count variable in __main__ +''' + +import threading +import queue +import time + +## comment out either integrate or integrate_numpy depending on which API you +## want to test + +# from integrate import integrate, f +from integrate import f, integrate_numpy as integrate + +# from decorators import timer ## this didn't work, ned to investigate. Added a timer to main instead + + + +# @timer +def threading_integrate(f, a, b, N, thread_count=2): + """break work into N chunks""" + N_chunk = int(float(N) / thread_count) + dx = float(b - a) / thread_count + + results = queue.Queue() + + def worker(*args): + results.put(integrate(*args)) + + for i in range(thread_count): + x0 = dx * i + x1 = x0 + dx + thread = threading.Thread(target=worker, args=(f, x0, x1, N_chunk)) + thread.start() + print(f"Thread sequence {i} started: {thread.name}") + + return sum((results.get() for i in range(thread_count))) + + +if __name__ == "__main__": + + # parameters of the integration + a = 0.0 + b = 10.0 + N = 10**8 + thread_count = 4 + + begin = time.time() + print("Numerical solution with N=%(N)d : %(x)f" % + {'N': N, 'x': threading_integrate(f, a, b, N, thread_count=thread_count)}) + end = time.time() + print('it took: ', end - begin, 'seconds') diff --git a/Student/rdrovdahl/lesson09/news_scraper/news_scraper_multithread.py b/Student/rdrovdahl/lesson09/news_scraper/news_scraper_multithread.py new file mode 100644 index 0000000..93fb4af --- /dev/null +++ b/Student/rdrovdahl/lesson09/news_scraper/news_scraper_multithread.py @@ -0,0 +1,122 @@ +#! /usr/local/bin/python3 +""" +This is a multi-threaded application that pulls data from multiple online news +sources. Returns how much a given word is mentioned in the news today. + +Uses data from the NewsAPI: + https://newsapi.org +Note: you need to register with the web site to get a KEY +""" +import threading +import queue +import time +import requests + +# may need to rotate these API keys due to rate limiting by the provider +# NEWS_API_KEY = "59e0f77b685844edabefafa4fdd8550e" +# NEWS_API_KEY = "a19a67269a0b4d2682f3ca8bbeec7478" +# NEWS_API_KEY = "00d78fd818ef4ad297890870a42327fe" +# NEWS_API_KEY = "96185018a12545d4bb56495d65ab7382" +NEWS_API_KEY = "412ab9eba73743f0a29a0e02565a2b41" + +base_url = 'https://newsapi.org/v1/' + + +def get_sources(): + """ + Get all the english language sources of news + + 'https://newsapi.org/v1/sources?language=en' + """ + # single threaded function + url = base_url + "sources" + params = {"language": "en"} + resp = requests.get(url, params=params) + data = resp.json() + sources = [src['id'].strip() for src in data['sources']] + print("all the sources") + print(sources) + return sources + + +def get_articles(source_list, results, thread): + """ + https://newsapi.org/v1/articles?source=associated-press&sortBy=top&apiKey=1fabc23bb9bc485ca59b3966cbd6ea26 + """ + for source in source_list: + url = base_url + "articles" + params = {"source": source, + "apiKey": NEWS_API_KEY, + # "sortBy": "latest", # some sources don't support latest + "sortBy": "top", + # "sortBy": "popular", + } + print("requesting:", source) + resp = requests.get(url, params=params) + if resp.status_code != 200: # aiohttpp has "status" + print("something went wrong with {}".format(source)) + print(resp) + print(resp.text) + return [] + data = resp.json() + # the url to the article itself is in data['articles'][i]['url'] + new_titles = [str(art['title']) + str(art['description']) + for art in data['articles']] + results.put(new_titles) + print(f'done with thread sequence: {thread}') + + +def retrieve(source_dictionary): + results = queue.Queue() + # sl = source_list[:] + + def worker(*args): + get_articles(*args) + + for i in range(len(source_dictionary)): + source_list = source_dictionary[i] + thread = threading.Thread(target=worker, args=(source_list, results, i)) + thread.start() + print(f'Thread sequence {i} started: {thread.name}') + return results + + +def count_word(word, titles): + word = word.lower() + count = 0 + for title in titles: + if word in title.lower(): + count += 1 + return count + + +if __name__ == "__main__": + # create a dictionary to assign news 'source' lists to threads + # the keys will be the thread numbers + # the values will be a list of news 'sources' + d = {} + # thread count + tc = int(input('\nhow many threads would you like to run? >> ')) + WORD = input('what is the keyword you want to search for? >> ') + sources = get_sources() + for a, b in enumerate(sources): + # get a thread 'key' value based on number of threads + x = a % tc + if x in d.keys(): + d[x].append(b) + else: + d.update({x: [b]}) + + start = time.time() + r = retrieve(d) + art_count = 0 + word_count = 0 + for source in sources: + titles = r.get() + # print(f'\n{source} titles: {titles}') + art_count += len(titles) + word_count += count_word(WORD, titles) + + print('\n\n' + WORD, "found {} times in {} articles".format(word_count, art_count)) + print("Process took {:.0f} seconds".format(time.time() - start)) + print('number of threads =', tc, '\n') diff --git a/Student/rdrovdahl/lesson10/cPython vs PyPy.pdf b/Student/rdrovdahl/lesson10/cPython vs PyPy.pdf new file mode 100644 index 0000000..dfbbcf2 Binary files /dev/null and b/Student/rdrovdahl/lesson10/cPython vs PyPy.pdf differ diff --git a/Student/rdrovdahl/lesson10/cPython vs PyPy.rtfd/TXT.rtf b/Student/rdrovdahl/lesson10/cPython vs PyPy.rtfd/TXT.rtf new file mode 100644 index 0000000..ec7e32f --- /dev/null +++ b/Student/rdrovdahl/lesson10/cPython vs PyPy.rtfd/TXT.rtf @@ -0,0 +1,70 @@ +{\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf400 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;\f1\fnil\fcharset0 Tahoma;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +{\*\listtable{\list\listtemplateid1\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{disc\}}{\leveltext\leveltemplateid1\'01\uc0\u8226 ;}{\levelnumbers;}\fi-360\li720\lin720 }{\listname ;}\listid1}} +{\*\listoverridetable{\listoverride\listid1\listoverridecount0\ls1}} +\margl1440\margr1440\vieww12600\viewh7800\viewkind0 +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 + +\f0\b\fs24 \cf0 Lesson10 performance tuning activity\ + +\b0 \ + +\b PyPy vs cPython +\b0 \ +\ +According to the PyPy documentation, PyPy has several advantages over cPython including:\ +\pard\tx220\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li720\fi-720\pardirnatural\partightenfactor0 +\ls1\ilvl0\cf0 {\listtext \uc0\u8226 }Increased speed\ +{\listtext \uc0\u8226 }Less memory usage\ +{\listtext \uc0\u8226 }cPython compatibility\ +{\listtext \uc0\u8226 }Stackless mode support for micro-threads\ +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 +\cf0 \ +\ +Due to the stated advantages of using PyPy over cPython for heavy computational work, I will explore PyPy vs. cPython on some of our earlier code and document the performance differences. In particular, I\'92ll be comparing the two interpreters while running a double recursive function to compute a value in the Fibonacci series.\ +\ +The code is as follows:\ +\ + +\f1 def fibonacci(n):\ + '''\ + This function will return the 'n'th value in the fibonacci series.\ + The starting elements in the series are 0, 1.\ + '''\ + if n == 0:\ + return 0\ + if n == 1:\ + return 1\ + else:\ + return fibonacci(n-1) + fibonacci(n-2) +\f0 \ +\ +The time will be measured using the GNU Time module from the command line. To make things interesting, I\'92ll be computing the series to the 50th value. Tests were ran on a 2017 MacBook Pro. While the function was running, a single processor thread was observed consistently between 99-100% confirming that this function is making heavy use of the processor to compute the value(s).\ +\ +PyPy did indeed show a marked improvement on this heavy computational program. Here are the results:\ +\ + +\b cPython +\b0 :\ +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 +\cf0 {{\NeXTGraphic Pasted Graphic 2.tiff \width12200 \height2620 \appleattachmentpadding0 \appleembedtype0 \appleaqc +}}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 +\cf0 \ +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 +\cf0 \ +\ + +\b PyPy +\b0 :\ +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 +\cf0 {{\NeXTGraphic Pasted Graphic 1.tiff \width11220 \height1720 \appleattachmentpadding0 \appleembedtype0 \appleaqc +}}\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 +\cf0 \ +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 +\cf0 \ +As you can see, there is a dramatic difference between the two. PyPy was 28 times faster than cPython for this particular function.\ +\ +There are some drawbacks to using PyPy having to do with Python extension support but they claim to work with most libraries. Based on the results of this testing, it would be worth giving PyPy a look anytime heavy computational work is occurring.\ +} \ No newline at end of file diff --git a/Student/rdrovdahl/lesson10/series.py b/Student/rdrovdahl/lesson10/series.py new file mode 100644 index 0000000..bde0b11 --- /dev/null +++ b/Student/rdrovdahl/lesson10/series.py @@ -0,0 +1,84 @@ +#! /usr/local/bin/python3 + + +def fibonacci(n): + ''' + This function will return the 'n'th value in the fibonacci series. + The starting elements in the series are 0, 1. + ''' + if n == 0: + return 0 + if n == 1: + return 1 + else: + return fibonacci(n-1) + fibonacci(n-2) + + +def lucas(n): + ''' + This function returns the 'n'th value in the lucas series + The starting elements in the series are 2, 1. + ''' + if n == 0: + return 2 + if n == 1: + return 1 + else: + return lucas(n-1) + lucas(n-2) + + +def sum_series(n, a=0, b=1): + ''' + This function returns the 'n'th value in a fibonacci type sequence that can + have customizable values for the first two series positions. + By default, the starting elements in the series are 0, 1. + By supplying the optional arguments to the function, the first two values + can be defined as desired. + ''' + if n == 0: + return a + if n == 1: + return b + else: + return sum_series(n-1, a, b) + sum_series(n-2, a, b) + + +# There are 3 variables that are used as the functions are called which can be +# modified as desired. +# 'x' is the series element that the function will return the value for +# 'a' = value of the first element in the series for the sum_series function +# 'b' = value of the second element in the series for the sum_series function + +x = 50 +a = 0 +b = 1 + +print('Function: fibonacci\nSeries Element:', x, '\nValue:', fibonacci(x), '\n') +# print('Function: lucas\nSeries Element:', x, '\nValue:', lucas(x), '\n') +# print('Function: sum_series\nSeries Element:', x, '\nFirst Two Values:', a, b, '\nValue:', sum_series(x), '\n') + + +# All assertion testing will be accomplished by comparig the 10th element of +# the series functions. +# +# The first assertion test shows that the fibronacci function is returning +# correct values. The 10th element of a fibronacci series = 55. If the +# fibonacci fucntion is returning anything other than 55, an Assertion error +# will be generaged below. +# assert (fibonacci(10) == 55) + +# The second assertion test shows that the lucas function is returning +# correct values. The 10th element of a lucas series = 123. If the +# lucas fucntion is returning anything other than 123, an Assertion error +# will be generaged below. +# assert (lucas(10) == 123) + +# The third assertion tests shows that the sum_series function is returning +# correct values. The 10th element of a sum_series series with default values +# for the first two elements (0, 1) = 55 which matches the fibronacci function. +# If we set the optional arguments for the first to elements to '1,2', we'll +# match the output of the lucas function which is 123. +# If the sum_series fucntion is returning anything other than the expected +# results, an Assertion error will be generaged below. +# assert (sum_series(10) == 55) +# assert (sum_series(10, 2, 1) == 123 == lucas(10))