From 5358dd85f183711dd15f1fb735502954fe26d794 Mon Sep 17 00:00:00 2001 From: Roman Mogylatov Date: Wed, 12 Aug 2020 17:04:46 -0400 Subject: [PATCH 1/2] Movie lister example rework (#280) * Rework movie lister example app * Code style fix * Doc block fix * Update the container * Make second round of the refactoring * Rename name to title * Remove old movie lister docs from the examples * Add fixtures generator output on success * Update docblock in the entities module * Update example readme * Add CLI app tutorial * Update some wording in the other tutorials * Spread link to the tutorial * Fix code indentation issue --- README.rst | 1 + docs/examples/index.rst | 1 - docs/examples/movie_lister.rst | 124 -- docs/images/miniapps/movie_lister/classes.png | Bin 34605 -> 0 bytes docs/introduction/di_in_python.rst | 5 +- docs/providers/selector.rst | 2 + docs/tutorials/aiohttp.rst | 8 +- docs/tutorials/asyncio-daemon.rst | 8 +- docs/tutorials/cli-images/classes_01.png | Bin 0 -> 37176 bytes docs/tutorials/cli-images/classes_02.png | Bin 0 -> 38614 bytes docs/tutorials/cli.rst | 1054 +++++++++++++++++ docs/tutorials/flask.rst | 8 +- docs/tutorials/index.rst | 1 + examples/.pydocstylerc | 2 +- examples/miniapps/movie-lister/README.rst | 75 ++ examples/miniapps/movie-lister/config.yml | 8 + .../miniapps/movie-lister/data/.gitignore | 6 + .../miniapps/movie-lister/data/fixtures.py | 44 + .../miniapps/movie-lister/movies/__init__.py | 1 + .../miniapps/movie-lister/movies/__main__.py | 25 + .../movie-lister/movies/containers.py | 36 + .../miniapps/movie-lister/movies/entities.py | 17 + .../miniapps/movie-lister/movies/finders.py | 50 + .../miniapps/movie-lister/movies/listers.py | 21 + .../miniapps/movie-lister/movies/tests.py | 55 + .../miniapps/movie-lister/requirements.txt | 4 + examples/miniapps/movie_lister/README.rst | 16 - examples/miniapps/movie_lister/app_csv.py | 49 - examples/miniapps/movie_lister/app_db.py | 55 - examples/miniapps/movie_lister/app_db_csv.py | 80 -- .../miniapps/movie_lister/data/.gitignore | 5 - .../miniapps/movie_lister/data/movies.csv | 3 - .../miniapps/movie_lister/example/__init__.py | 1 - examples/miniapps/movie_lister/example/db.py | 35 - .../miniapps/movie_lister/example/main.py | 17 - examples/miniapps/movie_lister/fixtures.py | 8 - .../miniapps/movie_lister/movies/__init__.py | 32 - .../miniapps/movie_lister/movies/finders.py | 87 -- .../miniapps/movie_lister/movies/listers.py | 44 - .../miniapps/movie_lister/movies/models.py | 36 - examples/miniapps/movie_lister/settings.py | 11 - 41 files changed, 1416 insertions(+), 619 deletions(-) delete mode 100644 docs/examples/movie_lister.rst delete mode 100644 docs/images/miniapps/movie_lister/classes.png create mode 100644 docs/tutorials/cli-images/classes_01.png create mode 100644 docs/tutorials/cli-images/classes_02.png create mode 100644 docs/tutorials/cli.rst create mode 100644 examples/miniapps/movie-lister/README.rst create mode 100644 examples/miniapps/movie-lister/config.yml create mode 100644 examples/miniapps/movie-lister/data/.gitignore create mode 100644 examples/miniapps/movie-lister/data/fixtures.py create mode 100644 examples/miniapps/movie-lister/movies/__init__.py create mode 100644 examples/miniapps/movie-lister/movies/__main__.py create mode 100644 examples/miniapps/movie-lister/movies/containers.py create mode 100644 examples/miniapps/movie-lister/movies/entities.py create mode 100644 examples/miniapps/movie-lister/movies/finders.py create mode 100644 examples/miniapps/movie-lister/movies/listers.py create mode 100644 examples/miniapps/movie-lister/movies/tests.py create mode 100644 examples/miniapps/movie-lister/requirements.txt delete mode 100644 examples/miniapps/movie_lister/README.rst delete mode 100644 examples/miniapps/movie_lister/app_csv.py delete mode 100644 examples/miniapps/movie_lister/app_db.py delete mode 100644 examples/miniapps/movie_lister/app_db_csv.py delete mode 100644 examples/miniapps/movie_lister/data/.gitignore delete mode 100644 examples/miniapps/movie_lister/data/movies.csv delete mode 100644 examples/miniapps/movie_lister/example/__init__.py delete mode 100644 examples/miniapps/movie_lister/example/db.py delete mode 100644 examples/miniapps/movie_lister/example/main.py delete mode 100644 examples/miniapps/movie_lister/fixtures.py delete mode 100644 examples/miniapps/movie_lister/movies/__init__.py delete mode 100644 examples/miniapps/movie_lister/movies/finders.py delete mode 100644 examples/miniapps/movie_lister/movies/listers.py delete mode 100644 examples/miniapps/movie_lister/movies/models.py delete mode 100644 examples/miniapps/movie_lister/settings.py diff --git a/README.rst b/README.rst index 0bb2f8db..596c6c28 100644 --- a/README.rst +++ b/README.rst @@ -157,6 +157,7 @@ Choose one of the following: - `Flask web application tutorial `_ - `Aiohttp REST API tutorial `_ - `Asyncio monitoring daemon tutorial `_ +- `CLI application tutorial `_ Installation ============ diff --git a/docs/examples/index.rst b/docs/examples/index.rst index c61c4009..244569b2 100644 --- a/docs/examples/index.rst +++ b/docs/examples/index.rst @@ -15,7 +15,6 @@ and powered by *Dependency Injector* framework. .. toctree:: :maxdepth: 2 - movie_lister services_miniapp_v1 services_miniapp_v2 bundles_miniapp diff --git a/docs/examples/movie_lister.rst b/docs/examples/movie_lister.rst deleted file mode 100644 index 594a67f1..00000000 --- a/docs/examples/movie_lister.rst +++ /dev/null @@ -1,124 +0,0 @@ -Movie lister naive example --------------------------- - -.. meta:: - :description: Movie lister - is a naive example of dependency injection and - inversion of control containers on Python. Original example - was taken from Martin Fowler's article about dependency - injection and inversion of control. - -This naive example was taken from Martin Fowler's article about dependency -injection and inversion of control: http://www.martinfowler.com/articles/injection.html - -Like Martin says: - -.. pull-quote:: - - *Like all of my examples it's one of those super-simple examples; - small enough to be unreal, but hopefully enough for you to visualize - what's going on without falling into the bog of a real example.* - -While original Martin's MovieLister example was a bit modified here, it -makes sense to provide some description. So, the idea of this example is to -create ``movies`` library that can be configured to work with different -movie databases (csv, sqlite, etc...) and provide 2 main features: - -1. List all movies that were directed by certain person. -2. List all movies that were released in certain year. - -Also this example contains 3 mini applications that are based on ``movies`` -library: - -1. ``app_csv.py`` - list movies by certain criteria from csv file database. -2. ``app_db.py`` - list movies by certain criteria from sqlite database. -3. ``app_db_csv.py`` - list movies by certain criteria from csv file and - sqlite databases. - -Instructions for running: - -.. code-block:: bash - - python app_csv.py - python app_db.py - python app_db_csv.py - - -Full code of example could be found on GitHub_. - -Movies library -~~~~~~~~~~~~~~ - -Classes diagram: - -.. image:: /images/miniapps/movie_lister/classes.png - :width: 100% - :align: center - - -Movies library structure: - -.. code-block:: bash - - /movies - /__init__.py - /finders.py - /listers.py - /models.py - - -Listing of ``movies/__init__.py``: - -.. literalinclude:: ../../examples/miniapps/movie_lister/movies/__init__.py - :language: python - -Example application -~~~~~~~~~~~~~~~~~~~ - -Example application structure: - -.. code-block:: bash - - /example - /__init__.py - /db.py - /main.py - -Listing of ``examples/main.py``: - -.. literalinclude:: ../../examples/miniapps/movie_lister/example/main.py - :language: python - -Listing of ``examples/db.py``: - -.. literalinclude:: ../../examples/miniapps/movie_lister/example/db.py - :language: python - -Csv application -~~~~~~~~~~~~~~~ - -Listing of ``app_csv.py``: - -.. literalinclude:: ../../examples/miniapps/movie_lister/app_csv.py - :language: python - -Database application -~~~~~~~~~~~~~~~~~~~~ - -Listing of ``app_db.py``: - -.. literalinclude:: ../../examples/miniapps/movie_lister/app_db.py - :language: python - -Csv and database application -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Listing of ``app_db_csv.py``: - -.. literalinclude:: ../../examples/miniapps/movie_lister/app_db_csv.py - :language: python - - -.. disqus:: - - -.. _GitHub: https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/movie_lister diff --git a/docs/images/miniapps/movie_lister/classes.png b/docs/images/miniapps/movie_lister/classes.png deleted file mode 100644 index 78db720d0181ff6b0531cceacf721d91b49af2bb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34605 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

`l8JYuKnGeT^ns1t$79Z^H?&Qx3 z^6y?PSf^vHTp!`eD{ptV?mx#|((`cAtSIsfufNoxdm=!*tT!9er)rCB|17W=5e6`k z{t;JTo-{@`@jtG>{}&I?eF>;K(_-p2-}>YLv)hmlql5X|n*Yy@c;<*KBIwomll`-Y z>g*_P!q4K^Z+A~$X3zBn@pKDch5-=r0HKKpEH5v=`y}Yczyfvi$8bi1==p;SUt%gI zCQj=1z>t%J5LKTP!^x?sf2No=XvNAgV=<_N+T-TI7ZX4+d-K6bW5`Nq7E^9#514)1KZLhnyVXG>4Zj8;*5EbOfBJ8&m( zem|hjAJTc7R2AIFZ=Q0D54Al$^a<7+Zn!x+We@O2IpyV{jN&z&5jzu$Ggm~ue20eX zXxTCIQn>HwSqj=GK;dD^jAA-qXyka}KZtt00HSGGEgvG!^dM>DW zE^ECXR>ZLLLvv1#iE6%uuliS(HoGhK4Eam#@4Na#QunT|cb=5V9D&4?h)=n4F2n7Z zK3H%JS^7RdEM#(~mB2!X9jXQ;PqHm5IE>7f{w#FerD8#A!5djxm&3o%^v%gA<|ci`glok6CuJ%) z;&gAI!A~VPk~HF|kP}Q;+BNtvMMsDae0&{2A+wl5w!Aaz+`C0>j#WQ9+f|oku8tYd zDjdk^5^%~&t;=l~6TwptkVk)O{^)jOKK^GhwFJSEwN4`W?ftq`k|tR(7w@hVCz8y- za{rDHLTG)!R+5|VMWQrOZbO>IyC*uFAH)d@SYhmBkQK!(Cc=!p6M|-gZZQwjR3I3n zjl>ug6@yZ)fvLVbgiGofrz!KY#O%VnR;8g?ZJAyAOvFBaLz(oz3|p(aujh{-QmXXP zKH)kldi4f5hS)2Wn#7xX3m&k^!VIEw!#~&q!VhAn0Xyz1kxkBzjLV#J-{-B!p(V*F zPm-AMCB-HuJ&R%ulj88usUda%dHDL3d%*3bK9I+S?@1jdT1@f5m^9&Fp0ieb8XLIp zYCI~#y=MRt;OYT9)$M%u3$3`ANPvLdSB9nZL6o^R_~Mcao>a4I@|7D{?SIxC4!mg= zOa2vD71Q~&e(d22aFa%Qz}z}tBJd-JuJLIJI6DgH-$s!51p5SZ`2Dz|f#uDwtfSJ- zQ!Vq*&qr$xwO2XkP*=sU{L-s8-g}+~xC6XWH-5e)Ob!F{$IH2Yl7l^V$%$qfzn80! zP<&HcSr{5!_Po$aNCM*y%~xgbgONZadLHvQE7lN~@J@G`Oq@sy)fCJ}_dEaaZ=fQ) zlQ%o4n_-Q!q~85*iL+-5T`Tov`Kl2canVCBOsw+7qHIBN(dOCi_7MwYpi`XYdN&-5 zma}o44Vt3{cl5w!N$vg(K}^F>ePIb4f4;Wn>RHWyT#5X1@eqA+9o;=_{am8^+mCAB z6V^r39a(qev^_=UfO>5(&+j?eRYzth4r73zY^CMo-ZUqz?}fYn)2T%dk(?f>B&qzy z5-qPz&A!3umDb(7pdY#TfD?IHoG9H#6l&XkvQbUU_s%-5Oy-tRi0e1JGK-+!y0cC;$?E34>A zl_yQ3Sij8h|35#rjc;V%CMAsRxr}OqKp+lJWn)iUD^EK~8+SY42S^AiD9R5N;TIG) y6nr2lC@v`?!V85;LZM>ZO|<{z3ol)49qoPp*I!WagcJi`0I4hMD1B6bh5k2b9?+ct diff --git a/docs/introduction/di_in_python.rst b/docs/introduction/di_in_python.rst index ad1dc7a9..644fef9f 100644 --- a/docs/introduction/di_in_python.rst +++ b/docs/introduction/di_in_python.rst @@ -135,8 +135,9 @@ Choose one of the following as a next step: + :ref:`flask-tutorial` + :ref:`aiohttp-tutorial` + :ref:`asyncio-daemon-tutorial` -+ Know more about the :ref:`providers`. -+ Go to the :ref:`contents`. + + :ref:`cli-tutorial` ++ Know more about the :ref:`providers` ++ Go to the :ref:`contents` Useful links ~~~~~~~~~~~~ diff --git a/docs/providers/selector.rst b/docs/providers/selector.rst index 883e4fa0..29867217 100644 --- a/docs/providers/selector.rst +++ b/docs/providers/selector.rst @@ -1,3 +1,5 @@ +.. _selector-provider: + Selector providers ------------------ diff --git a/docs/tutorials/aiohttp.rst b/docs/tutorials/aiohttp.rst index 3c90e6e8..623d417b 100644 --- a/docs/tutorials/aiohttp.rst +++ b/docs/tutorials/aiohttp.rst @@ -922,7 +922,7 @@ We've used the ``Dependency Injector`` as a dependency injection framework. The benefit you get with the ``Dependency Injector`` is the container. It starts to payoff when you need to understand or change your application structure. It's easy with the container, -cause you have everything in one place: +cause you have everything defined explicitly in one place: .. code-block:: python @@ -962,8 +962,8 @@ cause you have everything in one place: What's next? -- Look at the other :ref:`tutorials`. -- Know more about the :ref:`providers`. -- Go to the :ref:`contents`. +- Look at the other :ref:`tutorials` +- Know more about the :ref:`providers` +- Go to the :ref:`contents` .. disqus:: diff --git a/docs/tutorials/asyncio-daemon.rst b/docs/tutorials/asyncio-daemon.rst index a6b6443a..38e1c460 100644 --- a/docs/tutorials/asyncio-daemon.rst +++ b/docs/tutorials/asyncio-daemon.rst @@ -1030,7 +1030,7 @@ We've used the ``Dependency Injector`` as a dependency injection framework. The benefit you get with the ``Dependency Injector`` is the container. It starts to payoff when you need to understand or change your application structure. It's easy with the container, -cause you have everything in one place: +cause you have everything defined explicitly in one place: .. code-block:: python @@ -1080,8 +1080,8 @@ cause you have everything in one place: What's next? -- Look at the other :ref:`tutorials`. -- Know more about the :ref:`providers`. -- Go to the :ref:`contents`. +- Look at the other :ref:`tutorials` +- Know more about the :ref:`providers` +- Go to the :ref:`contents` .. disqus:: diff --git a/docs/tutorials/cli-images/classes_01.png b/docs/tutorials/cli-images/classes_01.png new file mode 100644 index 0000000000000000000000000000000000000000..ee4575a5d294e7dbda28e94cd4acba4ee4d818f6 GIT binary patch literal 37176 zcmd?RXIN8N7dDI)6%`c)Q30g}X&RJXMx}^EFrp&T6bOhkAwYx>5K-wM5K5>Tq$o{@ zln??I5C}0y69NQAN@yXG8c8Vc!I^m;XNKqB`~7&YkLxmC$;m!v@3q!md#!ujE8$m6 z4EO9hvWtg@XOGcEy{kMtJ4ie{Tjq9b0X{*FK9T|c+v;@g@;M%!(pbLbJKKQw$L%j( zz0AYoFU`aA@G%b$2l(jWI1i7{DIT85+dMoPDLgzv_tUGPT0A_Pc3gp6(_dR#o1C1i zudh!?NU*T5$jQm!a=E~$^78V*!9hhu#rgU9!@%FcT^5&Ra-JErS6UB#wBtX>r)Dhc z9CdlGfZ$o9Yxbe(?zc~I`#jbLJ>}HJ5*yvJsR)qTZ&B|CFzvGj*2?E^-PO|+ect7h zL}3O82kYtSJuR~fENo0`^^9*CvqtHmaxEvxPW1&h5+0t5A>MUOEO=7cQJ#5azROia z=Fm;QNOyPl_V)IpQgZOrYl@(Qm+$!p-&eotrseaZ8q?wtQgS=A+zy389XN2{;lqb) zHhc5t&F9XY`}*~(n3z~nQqr|+*Cr+=3JVK&?AUSd-aREHrH>y!?%usyK|ukLbtCAt z_36$CA7I&=?2HZdc*3@cTm;_iynoRG&cmZ9vHowJ^CayUdE{S^*DPa9Vc|UOg=5|!Rke+2?7gv!vA0X-c_=Ke$2@Hesp1&e~>GB zCrz|fSK_2w!IevAH1?y%m2{=wsWKaF17dNPmlk9Ov(A2<=_sj{3q8jJ`n#PK&*dO#o3cJuDR$H)s^+Ym=;PB;uE=Sbm==YCVwYsoER;yS*H@{Q_{)0lB z?dYPLPX(g-ovO1F0Vma>#h_*T{J6YQJF&nUcBw8e&}(qD_<}W-faHde2Hd|$aY7+Y zyrG8gR2koYTh-tHeW2~uvN*pWf2`@gjNunQs4J^QrW04CIFk!U^~M$o9vWX0E$^S9 z-F=ul9d>E0nf<^eMeCNmTloHwPP3b+e4oSyYVj}C$>ZB{7D(wqdWrw6V?-81xI67N_so??;hi@xy7J0m|*J+S;`1)I{9tvT9 zZU5cy1F$cm!FxaWhvHa{Q73Ofv9)QZU+;LU7Y@D~`y1~a;zue%+2BwS2s-%r@O9u; zdctoFpNG5|_{JazXGh#X=dTajtN_LC?v?m*?k4O0A}tE4FL$YPeOPc32)ccG|M>Aj zgc{fVQ$F^P)a3fG-;B|qki{L|`@U+Xqg3s$wM86QSReLW>Js`Ka;wIUDG|#0Jnrg; zbAVyuUk*r-&Xf>HPBrZxZeITD@!j8E4~iANc~Cc~Ta3)eTwjx_z1ZU7q78T8ssH+1 zF;Z5Dt4l88k%sO`0i1Miua1d+MdGQ~yZ(Bz^RpvkqQZHxQAsHOy5$>JLuBo{)*7#W z6@K<}Wi>JU+*ix62;JP+{-S+6Nj563>q~OnDQ+XlW=g0w@2dQko?udu@Dj)P&`skV z6w_n;Ddf*Rz9WtO5+CQ|R?AGY1T^DiLjv;L??f$I_Y>eWQZZ) zckxCz7*008Te!v91>R6VYH(qanyWd@{eQ3aNh|?4;$X9UYPh^%=195n$rW zF|F?@T3-cWf?|-9=h4o+v1NU0YC$~u$6&q*BgV|&Rz1x9eXyiw2AJuh5n1GKTaU_E zSg~9O-KQ|}Lp)HG3qPv+h!hcJ|BFJzR>2hH!-;AFjz~Let1HutyP0T$G@G*e#SDjI(&o{!7oTVlzR(qm+lKp7g|udt?c)2);ab)E zLbJS}^ap2Bb;c&VFxl7Ae*Au(Az-{@EHcZT3)NpY))I-!ipZ=48Y5WAXtp^TEXG`9 z>Bv_WBPOsjqHep^H;VB-sPyFnDzR2F&F?Fs!QG~u{1wa&<)NO0%LhZfs_&~LusIMV z|L0~+>CSchBIV;>gTs_KV)rIK#&9DH%k*tok}a%n&IS08D>9gT9ONRDu4ZJzs59;0 ziPy(KO;PQ!jU40!!tEmu{M{K<53WUKiaNQ73e}`OkVkzTaR8IW`r{2N;=jGK7%ROc zDLi>-PkmBtA(4>Bgz-CJF8SEZsnvdIKfPI9B=e&|4s zZNS^sWqAbM!7xu}nk0qL)lYR?-_+y-dT68J7Tglzh>N=`8sF-8+#OI>bjrrA>&t(=+$nfz8`v1)2A3Yf&=SAv7ZTA7($-aP$8xS)%Ez8%ql5mLu_5Q zLUYS(7eJf>?;Q=tU^*|M4a;1YBUctLqy1K>Nu9+z1xk~c}u%nr4L_SM&*FP?7xs=%8fn4-Mndi)ZXe+eS;Qool@SGL981RZl9^l-D zeZoLbcIda9|;+A0_0k=3)U4+_3jw9JMcSrbfK2f5j~2hMKWZg9eIA9HPCLg8L$iOH z=p$B8J69!0PT?=-Rn8lu6ZX&!3*p$JMkW@D{qL%3eweqgjxjLAc<=FNes8oMyMG<> zhmAU*{oe4#Q1te;LFpQNisG-sPzt=h@6W8bicr19AALqu0yk>w&K+shBXj+@v)~QDvbfoi-fkkML zYH+|(!hxlb-F;_jP~|0~k=EbK2hL?@lgAi`yK3PFPfGuBsDMdfVdCrrt)_?A>?ov~ zvmuITFQ#GUbbrdbJ>%B3<;pXpux%7Ct_1{*H!0iMM0d$;md|tDHz}FDx*ynLfb&kF z$|}w7h^O$yQbhMwH}NxfEkCAow>}vXLn(XrlQcR{1f(a`X~xCo;2Q3G3l;@f0|+ zc6=)u;>mp9+4`tO&+Xm2W(DeAv9y=clYJre7zAT2gs;@JkaP^jLD4~G{ZKk_U~SfB z`#~8}RrKo0QDL9X{nB$!NT=+en&%u+Jo%fhseDI|jse}99rw_@Y_}`Uki{BCzUUU?f_|!ywl-tFJr52m^swXi!i`1Oti#rA|jvr>Dl z>yy%?nkvMnTCL`cs3P-pePN_UjjO3C#fW(djR;;0Kf62&{|3!%$|lR;J4Q2hfh^Ee zT+8&UMxF5;bIs&vDAY0NHOH>z=Kb{m&*D)G*xK*#*bNaK5*FfK5~Wo(FPZQ}t=2m~ zOHs%EwA_Qf(`=rE`ws@2+b=1SstVe8Q>qPb=HJ~XD22JZK3n<-)ZsBa@!%&5bg3a6 zg2v>ps3`;`}6qJ3Mslu_DA@BSd!VP9gzu;LJR4)Am#mY&W87VBVxc~+jvt( zpm}h=?}jM3ND)|R&UonukTUAwgy-T7h#n?4B^#m(`#CRXJt|5@F<8bCKMtBJ{hcS_ zqNc~_Opf(Y9%d)Lr*_cj`X7FR<{H`*gP$xh5oDX_obCu%P&aX+=f|4Ju(ydVU_o$6 zbBS0pd`DoO(uoUb3ku`7yfHY=5%UF0>p%dhJM#YROxfw7(*>cxGCj1ljeqCIIbu z;ZnS2Yo9*anbOLsXJma2r^Nj=)hX8=u{i3EG!6OFIHlbqTNk@VwR3 zL{yfC&%N=U5R-6BN*e4?oSoAG~}{uJ2{s+PRq7_>fSDh5xCuinFsQ+{8SkkLuIymzF2 z=#h8G=8lm2Hu2%$maRfCJ-s$3DGW$C1+O;{UR})cYo%bD72}-pcGT5GQ5I5@Bf*Y# zF*ck_hQebn5(+xnV1bb}d~Z#!%~&!)W(=r-9$FjKc31>9zegg+bn$ivGSQEG{kwNP zwT0eQGr&DKjPKFIgi%3odzo+a+~WMhGwIXM)gkdk%E(PN?B#oj2E_@CBWD|suuR6G z+4?iKp3f)W723$p$k(yoFBJ)Lcf5`)g)sfhQ(xZ+*$nm2^ zbswyLC2`)uh)bNpN7dZVh>%;<*=(n1 z?8K$tSnjsTGco!4c`;+W^`^X+FH%Y9I>vF5Jn4prd2_2sE!^no5zil!g%XcqtA6|3 zF}G5&oMg2J>ZrbSt_svVE}yXcJMvkv%6f9x5!WR&G!S>PD#iN+Q{a?vEbj%GB&()a z|Cei{117Vc&lFRwn-Jw73G@3tr}u{@&LbM!F$2Hb$%~qqd7h61>qi8AjeNXVs2dcf z3mmZ6VNxwpvsuZORU3FIUHF~th^2~9eFxNlkz_OwSc6=XMN#D3Wh_`MoxwfQ4^&7! zNEvk~{rkJUo$@AO+`T_zyYIPgdflJy+SrJ(tM*a>%4wQvqNN_;Xci z&GpH!50PMTHLH}8d6VSeqCp$qx`&PRR@dnDJEam_4@WUlTd-tj6+S1%z*9?Y8YK)r zPW7qn5t>;ER`-yKxKhT%1xDzS0?M4EsGvFb*4p>la6?yhrB4I*Wz+U9e^snLI_$<5 z20l$-de2`&yXR$>=p$UjG^;1H!tZ0Q|4)<8LM6kpe!BFT4dns59Hx`#bclHd}lu!Vg`dEbxAc9%Jz#m zm;Ue!`B$yXv#k0HtPGy#PM-o$IXaO_p3=ftvg-8xC)}$D{$$&?mVtKd3m@AHg6X?I zjK)J;Eu>RGxG$yE`$xcvjV(%+v&)!9N{7!=ufCl0oRAMn?+r1J6qeNo@Zd-qO+h=7 zy|AlgdnjnjgQPtlUd6>MQoZ7OVE$Zt z8% z>h?FRE!!{Hl4}KaL30lQvz5a&CgplGUP6ak{{gCm-Lqu{cGoRWYrfbAyR{zbVRctu z+?#2o+Lx_M;g9(qdpzZUVXe)JTnPhD@4K6IGa4_j*jh&pj0Fi0{y=T6gxp_{&yFV# z))js~81x4`1T6jf@*)9P+Y0o?uTs?BVg4E1CJBge9+*^%a${29nv>suL7nH?u4RlIBfOt`lNGXvOpYS)4=2 z{Uvz;v-lGvJtbgeXx;f}iJ&z%%K87UOnFE0XYk49<%5QQed}PkF}*cQDCrjj>!Zly z78quscn`zpCtfAKezQTrW0qyV;#N(7k(EIljGlr7h`({`y#g<-uDZ4Tw531rvA}(b z6mY53U#niS4?kLb0A?rj2ij&9avz!$uGqW}h84P-m0$VmQh^gEFezMV!)waGX_ORx zYQt-|kT+T_^(lW!vGZ3;j(v3QW)chw`BQw8>nYZMki53z@)Yp;Zv3gAg}-Fcw)Jiz z=EM~=O#Cu<_n5~c)=8{h=|kZ4%@=>1{cV+&`c#L*l)9^&}->vo0AVJ{upx~{Ny9Y;bWPUIF;e_2Em(qSGM1Y|3!0V-5Eo=wKZRJb6 zFVbM*CSV9R+U7Y5J8)n@+w;(0P~*~Zav6mDr>OOazp4Sq!7nF;YiuZwx$Q4VU|qwE z4F!S^`L3jyr_d{T0UiG56aTxH$YPJBTb3EfD>sh*gdlgkHT}Cdw}o7RVZ;Jrn3bU;NA~)Lna$3qH*Zlk2KaM3)Ej`_xFK+VTykMxBuN4 z|4#t7&$a(HeL0`@m6a8KW}WU!{vS+c(SZ#x=Rd$(ZFJT>$XtGIsRK!Th#OEX(M{ee>#*@ntdvPL zNEcDaKy5k?1AN}LjfcqZ5a4nrI%*lLK_GIzvlhAI_>hD2QJ7Ai zZOtXI__f4X01`&r3ng<+OX3~a8C5lzgaPl2k_e~C;{qFKiBDtH#%K!>UiW6_r+}I3 zl7~xG=`@-0meJ2?-AE*I>6OyNdBVL+a%P|ig4pcj$fG2<@m>6(besv;W5mKpDl~jR zx>;zUi$GjiL6}Wl-?U?H1NGeXjO;g$AHi{kro6Xg9EI6v@+J)A*$QPfN#=IcC|a}R z24zp|+MxPyYe3u@*)a$VsSg_XEUz_f&&i;5!=_e-G^O-T{Ba`x&01S5M>tS}$7pT) zN+8uoO6AoDEOB1kCHCJz3U6@#>gQ&~XU!dhPVPvokMBm(1E$#xQw`$(`N4(hjCaT}E3NY;21z`j|tkld>jWmj;L@ zNQHlf7=K&P`rWe0C!r3uwgyi@xYrwPk1w)7Xo{SB5)Sq%i-d=3!NCiJO4~>!Ie4Xq z$Jr%!AdSoXpW`x@KIjPx8}}a-3b@`<+ip(a9`|S;d61ndG`i4S>`Xv57qGpgXKFyWw3~4$e6AUxq{PX6u6TZp_?m%1Rh*;F6#nc`mk!xa_9$$AHMLUX(0&+c17+Uz zi}W5Xt9MB=^1Z&=ASH_Cp`{B*qw|b4$-<9J#AC8k!jwAuwFR{ zQ;+!M&Bgm*I^3AuFE-j+_zW+OJcFFv-{oi{)m)seGWRXf|JnfQHwb4Xg}zTjzaNx9 z#`AUnRkCD~TwhehMl(K(%-7n)Em%peidz^{vaQc?uxai{s>k;bZ0o~&Y#ZL^wj!qt zg;CU0f@;b}M2;zywpJ?4Z!n*DnsXjJS=G)953Y&z&|$ppwooP2P$fv{of}BXCv98^ zsq8vBoJXXMbPaSPID@RkhPuIlS4wOhqTifP(BP*y)R+r5tft?_7czB$^K5lsfLTyE ziom*-ZlqLPXkZhi;;HUNQg-A=rAd|qus!#|cn1F#sC2u$uoG=T4x*0f3BRlSm5S2l zb7Y*bLVJY)fB#8A_{VW*?vcecVy1$>gEk1JfQw*KjejLa0Qx7M@i!KVH4!y1&0@i4 zuO8m40A#_31|^DT{z-KijNaBG|9|H~sYR`f{pPg)I}DkB$l-sTtA~&itE;P_szRdtrJlJua!tOQl!Tr|6~hUe~~qIdKXZ|eU6-P zJ&ZRSG_$Qu!>tmRaT#Nu{hDZDjI^vd57_El?S>&Da9$Z=5Lvz*d4a35{`Y!W88^RK zr=DEwx>`~%$6yj$Gx2qmm6d5PO|S78-}|PNHLAN<`+6Iz{zx5@#2PrXH5Xw!GE|~7 zdc#MCfUMX#_&=N(AHcCN*HCBFW=4ZqzCId$Zb>A&hN)J1T&x+CP6?FplVcCR9HgBs zI2i`!62b*+bAhVxW6&_+O@Qiu30nYO1CX15$Q{TrZ!ol%9Wa=@~^B*9`8O_z`>fI1%}O*EDqKQ zaZx&T#^zFsZTT$=;pynW4hC(h;(vJagD}0AAL4bKcpyWhnU_qnN3M$LX~iLL>E!Ld z5O|{OOQ;I*#b*d2(5Ws*q95y`;Ss9j49| z8-y5QDgbnG_wSZJNnuevOU2kv#y^idXGf@QfKMGh`3c&z8G`mJsGV!GV9O+;78$C8 z0kpMAW};D6W{PsgygNPknxu1IH_y@HuEQ59OUX5Ohk*tDrh6_sN{5k0U6vsCwuGv?pWZWc2sL&Y%A$GjARHXjp+$Hc6wtwhb`gy2+xqd*Qi1YAwN}mo`BM zoqSsI4s1M53YzQCTaqBL=#)jMoPRrrXwk>0PHy#3Hdd>n+}MD9xPE?vNHwIz7Xhh4 zFyB0;Bt9!6YGo!ttFBLe^|W=5{DP-O8kr6Am)Q&br29`Y;(8nt0k$ohE7VOk5N5wd zAZ!OBugz(kHjmw}YR;LL4hq-8WjnRvm0>`%zgWAKGLz5mF`M>f^G0VDAS-=gzdWg> zuK8%b{$p{p$lpWuDpHp*C~a*uPv*nt?#{=Ncn&X?~SC)%P->B&zztNFxEkV(X@Dl^6=I+A}H#&p9&lqz3(_Mth{Oo5j?T zqD)KX$p~56l~9<;B`lqq%9cpOk=J41)&2kEa!#H3(oNuC6$vFR+2k}$9GW(am7cgX z>G&*bq zG?;GfRFNcJ(cjT6tG+N)l3HyuEgM;Bz&|=^X}%~h)w!@)>L}|c1$F)C0XUu31tGOJ zlD+*FY#8RUkzkJjVprXZ+8)c@HQn8hmR^pL15QH?Xu6hPRCQ9sU;_a^upi0A7G0uH z>B9glXXEg-3@E(G8oBSvqTWWUSDVJ?eN-Q0d26Ve#mbApiaVSTvsp`Xj9p{JVW5us zznG`qpG7&$l~pbxQV6-?vo>t)jz=KyhBkGrt3GVcHRi`5A4j=M>IK!t=H;ck_lNyv z{B{&dujan_5J|qJLVC$!<9}dW0uO}JU;|9}#8cyDvP-giG>SN{-rCpKH@QcWmkH)K zD*KQo%MGW!bde!lJNyr}YBZ{tytwR0Y@%K*s`NtRjpFkbGS!_Dl5SpN>`tBAZhlcz zlfK}(ISwlAiQx1X1ANO9|GtZxAKIdL;YKt#)UqQq`J^ExWHd9wF~4Z`0z^(Sb~+w3 zTw5fAjq(7B9OXZ`uj>U9#VBwW_WbZ`eGJnP3u&8^Gr;6YWoaWS$-A1f%?`ra6SH8E zjF)TTq#t7cn66kBJs6U0d=xe^x7DPn7Ats>KUJzE6Yk&t?6+D?%?#O$|uieAj zuFoRi6FodU23vL6xyZEu$9HlicvoQyd#o(PIt~G0JJ1> z(i-<_;)6KUdx5Sn_}R}3vVX?=g{JuT%GMJG|KEGu&PgsW|9P3BRU2Cq^4=d=DM z@kpm9w6tkTg&!ZSSXtHxWk8`@v0g?pt;SRs`8)rXTK!3O?v4UuV?}80Qk-WWqB)cA z=5yWPoRy*<-9*j2^D7c*HvS%h#BRQ0?gfcbrjdQ!7l+*p9oFKpmjhEW9 zvfpFkNuoRRW1!9%vDM%d$?RGe(~L<91(%^b&9+(FSCN@#qa*}#Pd{`lmj&)?S-9`0 zV}d(Gee1NqPrzQ7gFmSDRdMxHw7BpWC;AskcH2TWnHueUzgx2cQ|;zzJj&za7@1+_ zDV+>f=%_f|7tUYFfG1?cyJ=`Cx1`f3dYI{$K+|qhYV|^ktp`4~3YvTS8&ybl`PhLp zO}(UMqB&}yJvheOP}=vU9IZMl8my|J&~t6{kMW zLd_o+47!TR9-GC8W~S2N zh~#_}jD4va=Y%S}*T|5*B}MY?O4FzwxZ&~Mj~IaVqbM#XyEDqe!Q!z1XI81NOEXn8 z{kDIB4#=r8Zg~X8@qeRq)zlXwOFzB=&;AfLigWaq%INe3S{`d5C<{lPd4Ba=()rA? zB-vX77;Sm+*7AuS>%$rCJTng2ZSP&tr}G&7BZ~*Lr+&Ndh*;4QQ(xDgvS!}G?Q>{+ zX+hPEwr?lcHi$Arom;Fet<^vnhke99`bj!C=909^trP@S$X~4;8CdqkF0;DRN|<<- zx^UOMEANURta!S>Hd3|k^MTiaS)rU^t(KTm6PPA{{Uy`YueD83fxqQ#f2JKb!oXBd zJ{042%i=lYsXNBLGEQ}m-Oa0E1Da#IrUa4%G+pR#kf>|p8l~LLzPqsG=;;(k^DE|O z{l`s43Ud^yrOdutSO(c==8uJFQC08f)_OViYQaKND;%?Xb79 zN^q;Qj_tjbH!WsE$ROOk<769gw-Vr$`C$V~`W)j~A%W^LB(+eBOs8z}WoF3zuM;Nk zniVI90_p8VG#>WFb`2=~Hb*lD>eyG4tuPbP1^v{VTBcJysoU)+*IIT+MB^y;cAfOU z&xWFXfL6TN_#3IyNpIW&8Xw2H(Hgy5yh|>4l?Yx>j>qp(b+0EzDd4g-wBN+Tg$T}0FNgVfcg7dk5w&jY&2)BT{1=87+8RY~$U ze;l`F+u*M`7M0)mgU|;BAyc9e6%L-{BT7bM9H&DeEiU^xw=p(HLSe z#0YH^QM{XBfawoKrA#l8R8|@JH~rA+HErEbsQ$GSk0oz3K2Df^!(3mN?f#?ZD$ALG(B;PIIpBTw?K5aC#KSDyTg?$@~+i1xG#Axx&} zBVoCMwbu7aq6lm61Eo#95ac2%`s^YJf&$I4Neqa#+JZ`VuUvwERTsCBo)M*B`a&Zi z*q~MIE0pf+nAmy}mbwD>*Jqn$X~AJ9*LzETtuB8(2Fs*5j;fsP8pukAM=W5#Qf61h zM)be#-BnjfbVXxQJ>kl#BrB!BxDS^-o$B&wV)ukP6EMqHkHIR8vmx)JqvG4~VCDzS zas!M6&ckt_xyKNU)e3u)SK?!(Vs0>iGjbnJAYg|qq5EMccmMnG6?wfcrn9~FGRn0V zK$;=1=9yb576WGe7o<&LP{)cDO(k8LBcEU`Vo^T*GthNG?Y<}LH`!i}oIZ*&=nlcn zyE&s}e_WyDmIN?NxI}G|$GdC3eu~DABeSDw53;@i7~Cp*@yeQBI>??aPIcf)4lLu4 z(<$!Xo^WHrz===)(GYbs49siNuBMEdKUW`S$(}{{x8+S-@7oFHaQt!ZWgb1owLo%Q z)ZcY|)*1kt+Z1&OZ*bZ46MzYkCvJ(b3+K};*+*0;%{`72dF`M%t)t({&b%x+V=q*f zaoNfY>X?yG8Y>DpK*bFBfdydgh}pNPixtZ zjS|?LK1B`};WJ%4iZeG;*XZ+S_Ab5ufTHG#IEFv}{azh?qK|5oHdTbPoEgO(Rhich z)s*5BbEk?Dv~@}8Fw2}lA+(ScoGFXJyRIzU_}cF7@KG5>o;UZ*mMYI4v6z;@m!g*M6wG&~ZJA4b z!oJ;v00g7$lcUB(`-FP2xCA2#!xTO}JdOc^Q=bAwey?UwVgMsNNi2J=BtM zozta#FvIjihVrbdN8t}wo#vR@SvZ&dM`br-K( zVQFc+6$4Jw{aLSL!g4p4ARx4VYa<-IAvDZR|4#+D0dq{93fc z_hxDNXmsoI3Dq>ox*2}4?n_8@$uZCMo{(lJaL-0{$J-SO!@ zlc5o)4~ctcamX5%zcPYj6+zLF+cs#ra}y^up?FCazBXTi`x+V0*ihv@qlM20al9mt z+qabpztR+A!G@Zaj;E24)0T5h9p!U&v=2|FB~;mSSjgCynm$){S4iJ`obf9)7IW!^ z7U`I_x1-LZV(MXHodI#bU-CoTGBPy+yx3qK4i0UlW=~R2KK0&}9d+5*Vw;-zB)(kW zAwRAQ?*q5jU6PTZ7ygfl0-1{yLL(QC7_r$yO&D?COnqCSIdt*j*Z}^>rEa3#_af{0 z@zviUt2;d|v{Hg6sW9`V1UaOjp`1IxP}4uhU(Oxg_;GJnZHBUwvTx?->NXr={0akc zrB9dez1%NuYAD-vQejY3JIu-5rym}b*?-J;m^`d~Q1fJFrpJh&X4d1mV8K9;VfQ`$ zM%>aBW(A9x*J9TPYJwYAj9fi!h2_3fA_}nzJxBHOt6C(=}u!nrp2~n zKBJ4inKV-=Yh>P3O%>(_N>0}g=?oB^|FzT4W_q$_e)A= zs(O6d_)o}c_*J{>VRrqqYctpY)1*DxI$bNPHWr|~mxe@seFPE!1I)%>J3jYR8RaU9 zS|Oj`{OUV>BI4g)DA)8ZNy}qgQzM%>n^oj030Mh3r5*m3e{L6rRW}z-PZn4k=G`{C zB$QT5`R=w9{Vdg6g&=i39N)MkOFAn64+#e!dJ4}m(GZPo6;kw87ZpoJx?Kf*y944RiEGL$Ylm>;p=h&m-14VH;bc@y^|D;u zyM6>fNjstht*0OkB_6y1cY9S9I&G0@;$AJ=gT^*!WQ;vh&`Q@nRPD(>;qo=`rg2sk zt^dZL%8$)I6Uw<;BJ=vIk9x15)tTWX0DVTyL#Bn1D?gyNfUGDfyBOc630ExZz1!lr5z99@rQE$AmMs`(t!UR1aIZOOn$06CC1KE3+h(=F&I-2J+xkQ_?fu z6bE#35lT|@)iGFmx#`#CsN{}_1Nw{rbEwgsU%*8G^Eu-o89COZW(;g9dfePVe?MA2 z;WgvPG`v#?Oq%b9afaex*<`KZv%15h>s_<0i@*J?RkxM(u>o$!JBN5iRtWKridQpOn@AHVXT#7Yk2>BvuEMgXZk)6M7VsQ@cBpqrpU= zCn0s!)?HaN9K7lHe~7I=vxYGM*gO7s_x6?fncx&Ti8UFw?vQgwBw}RpKCULxqTxU& z%wM8N5z@o7IS2Ky_#wM7FmDc_ z5+og^W2bbvV#?W0rA&tZT+Ujxm#XtfeIg%W+_T~&7E=E2Hs`bYm{sH6TCvfO$rkYw zd9qlY4)Tn|ePTBF?@Lm;i#8UkQnS6qibX{!BdB0&T=a9J%ueJKge1VaS0lH>dB6 z1{y}AIJs<7eT=JN)GGb{C@3K}I`ao>Nv}kXPY7rr^18I~W(5tz_uKA4k`WHSs4Y%|0{ zwEt^>xCnrlg^Ew_^5hTRR_T?xN3lRp*pr>Tteuu=F3S&K$D>ebOnj@jKGcByozG{! z!R_~tFq2P;;b0}a*-O*-{s!BmNbpC8$&+e1Q2He?wQBTfy}D+;v0gvU5mlvB0J>8FSB|LfNY#y{AU-|xM9 zAL!I@mh5>wCD>&;ceQO7)G_ZA$yLgf0(R2FGM~2Pt#5FW#AMDr?r{M~IXz+a@WtWAmG-m})#r={o&7i8 zo<~ywWVe+$QxGbt-cQG=VK3bCG>aM0L2D7=89hGu(P7Pe(3M7P0qS-=z)2RbB z38kZ(vdw&;7{+CrLyW}zFJRahr3_6^*li~xxnw=et^dd&6#&3H@Y?=WrkdBrRNSgV z+1RlR>LT)oJpT>vOxP5G)KAm37l|m5R|fP<)?%a?R^^^ z@E7Y|+5y~k3B@Gk&N0`qY%-(K=ob$H zAjGu!Zyy!PyB8Sa-yy7rUDFSoS{QM(1F?*kwmq3?($8d^W-qdv(d_8kP^2QruPoTXP5UuLo{VnA55BF*WU!Nde6E3Q1>S zKBQ^j`{hOZ36r|yEqHOR^5?*1mHq=T71ea0XzYU}hxZQJB?ZbygJY>P*m&o?6e;}}yR-Qs;8$z!9f3+a&Xxow3{#a;I-gp-hF4^$PG`P%0Mhg1KYJwBA>|vr zziaZAsphV6=jA!w3DXUhynXCeQw^G{I?kxlAwI&up^Dmb-K8weNOozy) z`i$V*-Lv9xFf5sB!P(v!lUVH6qDkk|Y?WH!d~#o{u&S}wxm9X(PhwiyEp4B;?(>iR zM?a$tF|XLN=7Vu_&u5`@8@Hr45`jGQJ%jl)OSV_HiFro9gJ7p^TGm7iSD9E*0CdIP zRZ|*AyBG)~H^*uy;B71X;qD7+&Wvhs^5WN+p${LE;n_gT;Zr)AF*`*{Z(1=*Fh1%v zf208`F*oUG|I2YZ&Vfq1xj7^Jp9M{O(56=S%3!(}L1|y7FmHHT5A~(yWQkwLqDHPn zgf8c^iC{Tn;l{-7gn0{7$C8+4*-&2ks849nsf=sxJ;aH|*{*646nX4DJ#kCMV+}`- zh@Q&hVtAF(T6&wC_X#L_F}SJj{j|*^w=ODq<|S`eS1~So zfAyBhitf*=6}p|RB*k>E4s}p0*o1qwJCo4$`MP27m}}NMV$42Xl}jlPJUs>nzMUIe zJm=O1uh93`ccMxd>tU1$&a+)*0W;4~VBUL%*Wc4Us0}Qifq4AsG_^BhAKnAtDjk)i zFI;+upZ?jM+I+5WF;u=XcWV2Y8T6QIv?{grtxILvx_i*7G&9M1;wEZy45sJ0FM8D~ zQ~TNh7{^hZX%9}nmAE?Q$xQj&#Nd^!wx})!!dtYJ4PXXP|*o+P>UU2lGpG2V7s-H5j`^wDQi-TqL<1syF?gB)?7tOE@Mk7O?3 z22|;WK1#ON>Ox}#vgViV3GnfAbw0#TF}OXxjYpu4@Q;2O+pG1^c(l{*a4wWxOwW9e z*2>I{Tx{5z?4Xv4SN3)HCBJGu4j>JQMdz8{{Q}}D2X7!Q#-~XStYatp^&1nHGfG4A z=wEwDi2?T~FHdprz1Ej88m^HQNN}QoYxW@zT!KcE_B(l48 z5Nw++0=uK%Lpx2^WDj5MI)OTja`RIKNJ@qi3_l!NoLVRZk!;!PbBi0kJr(-8i?FQQOBaz*k(p-QL96+PPYcEvPS|E(j z!c~>;JKw|3EChUOH8I2yiI3FWPk=C7G9d?{@TG62;Gt(wU_6}85 zOi!^628@k!$bNe6O)u1qmhdiV8FF=F4zEBRo`Qn>c;uOR_lOIq+@*PXOZI6ho@|2l z)QR*{eFQ}29W0y{$@@AOUoJ+Ei%@V!$LZ$FEY!L}uDb+)An$(K0ln8%TNIZew(^R5 z{_Fm&M+O4{g#NG84M@)z9&2UJbr#5*$6)iD5|1hiU1-^W1aKx%ate^G66TGxM%lGqYyanzepw>A`31B{H*uI7&gsO~x7a6rPn zXl=P;Qit18!i3tUm5)BF^M0^&@W}eceW%W3cTm{gnGBTBIg@rBTrNnOc3muEk_ zuGIV8p-pkudgS1XW7Cn^%yoe|UA!M6JAtb|kw0ZI!!1;t=fY#3?5jOr+=$pRAtRqi z7Vqn|Kip=r@m}%xxJA9CKKGpI3B2lxR>ill;l1gipEVvy=a>;Z%jxFluNtkVr85XA zefrpSQBP(C(1mrVb>2H~K?Ov$q3mG$*}1?gApzUVuwOZdy=8+Kq4GE1$NVI~s&aS$X7OZ!D8SZXdE`_|@>9=teTlX5U2fbVjm5-WIwOYYoJY&T2E(y<8X{y!CpM@I}> z=exU2%>z@m&q-4k?4lRs5$~0!JzY`nvu}_@DpHZ4uQ-)M8uWtWkR>^2Go%7m!X{C` z_CDSZx~hVUfM3E`9Jl*R&lDFl?(0w8bD*>%@(c1O6|`jxThU*| z#cdQ>H?djxn_KFgZ8nJ$!2UA_p6?8}O}s9+Zmpa@tFf=w*{Cc7+9&#P(l)o68u>I& z)r<um9CkjXSIb(^`S0ZN76vx+UOVwDS%-HlfMJ?$_AX`sjoo>_fgi&Y1e$|z9 zN^m zP}VdWNj0 zx5%=$ll~J}o?rj;+xjDh_7St80Pc!8X>n=J)$J{N1^ZK(H}-8?Yjb5z(=&p-uey{1 zGb-kZ=A$G{6~+avksr#G?%B<5e8$UVLLVEdva!M_D5rF{K+{kFWIFp;#5bz@`{%w{ zkQcEI*_N7>eg3piJ-t7rNU1F%^!C!pa3ko~Up@^mG?il50ZnF`Q#+A;VauKxf9 zFnqiifF-Lc7Hbn|-zG8@}9S3J##`VwrRD z5E~~)`Srfp#n?XIlzId3T+opAGRzUKF#9HL{hMnyepe5=rS2O#KT^ChqTUhgP@1k% z(-a#H*`E6u5`0Ddv-4tvblUIX%m0R;2;7lavDP%)?w%3k6>Dnn_7$s~vlz|oG6 zk5n@Nn{}dT=gQ4gx4>AJ`{Ak6H9F?3S?%AA#>0`sQ3r7c+5N+~(|_VQ{!d7fro+mH zT9#{I%d~e!@7}RbOy*@&-Ap2}F~MvG{{}dD$uEiI({8IsN^A$GfJEv9$VZpqjFNKX zf7R;0!l(Rqdf)%Q3a|MW`Tr!a^Y5S`{u#Jw-cXU#B-inwr;7(RY*MeULMvI^+%StL z$QOWMy$RsF8r7OA9Kg6LIFwSSf5&GHqT#awP^jby8b0d;y;&A0XViPKu7xh`aT?oh zJOBwh)#U%Jj>|SU4Djp5toX!iF40Q89j(~>R@X+?88~z&nnHgNf-TVd%p8+ZQS zXm`EZ?0(i{(Lk~go$M5Jzh#>ZKzc4(1YPYw{AyqTzAuvEsn>M&35bmbR^U%35?E^U z7Z4c=0DFH@-Y-{<{Q=sYI12zy%k8nV!uF$AmVi~7K(^KHBxcg@&H=4=-gz><#(Co2&(8l zWd&0Oz?)lL14^wdA8Cw6HU9qK;xZcg4AltWnX@g->XO>3S()kRBAOecAd-k_p52?O zTR?x$05ucll^RbS^p#~_6X*zx+yiaK1GA&S1)zJKLSMQ&lUW{5HB#N@33B5C7^$l1 z8Nzzd(Qg!m()F3$td$@M%=%=I8xI|wMh#VT4IAGwxoa4-0x8ew>`N_NaRGQQWTHL?ww{#$VA_X>oyfHVnokTiM!sDe ztmof9ue+DFvP3H<>DE>91rMd8V=ADE`_$|yBTY3lfP%3(S>JsPX;UPDNjEv$IG zp?*Fn_v&vnGf~nM&+YO=BX`3UFQ*-FNR+Azbli5;;}0ee0IgK-LpT-5{wRI~o9=$y z$`UYMk0X>C8zriKuGq|$96V?jRrr(M=z3CbCP-r5z_kS+@Wxt62X?=ITOPof7ouVU zUjE*xmc}Ax7%*#1fX64OTOiyXD8JXuBOQQ}XOb`7ZrPTn&8A2p;NN^&Nx(>-y8^(~ zhen)(0sLx?kOl_R=GH}^uv8W>auF8{kJHiJiutSfe+SO^Q7&iUc`#ML)U*<#vKY<+ zBIDpYC|YP8a1R1z&cikdM3m6_oCKx{m_b$y)Z1?nI|tIPpMrkGX#*55CGEQ<*z9jp z*{HkQ+n6d~0^xV|!!r$kMnEj5ecp?ImNSQt zEXhNsgQx3?57D`-;#RU&ADf(A$m-f6r#sQAO=kJ)72xk4@WIi;s_) z_ua9)h~f&S3%$E@`8(!DURphrl_sUqeq9Mi-fPTM`f`+Rh{+wk3JP{Rijkx0;T8lZ zIS$c1IwqC>WD)fSj)JNrrla)?g;X-b&D zJuqD;!<)Z~{Kc2QCc|Hg_aF8MXtGt~_Li zu)T8kkJKIBn_{P`is`Hd+!ny#5|G9Nvf5_iZl~`tL-1tMr^QoraLQWUPKBJR`gO0k z@E`WuyAl|uh+1;xQwuQNy>Hs9=1$*sk%9vdSTsKr0{R&~ZN*-UVTgCj+c>EKK3e62a7wX0mvT!B<~afL62yt?dZt zLkz+xD@xM*tT$_&B<;~x4;ywk?oBc93B2QwnLhE0vr?Zi@k@~iosb4D5|SzwUK<5T z1vqo;ky7dD@|k;J;Io+SHJzBCk98&Ig0PP@?VYb@n#J)cHz6(J_}asvhd-i+o(+5C z6%IW!1t@*ZDmM4`IW0O*md_l$4{n?9zQ5<2&*LC>mt{C_veD76T=$^7)wPf>M1AvQJ z#ai*hj7#ri*-FmCZY6Ab#G~aO*irbaB7mbJZpb-F3DH5h!;>LFQtg4A_eIp#-^BNF zgs5^Oj(e%Y83)1TdYlGIfcZ`AOU_k@@w^`y~;W(c3x7ZF_?1vp)N|)Pnao(TQ$o zna-_?Zr=RTt9ie8{0W9RfH#w>KxAU%b2%;4W)$rRtUF|vbaA>ISL)|d&OWV3v8~uc zIUs91eD|SmMLZPtf>diBR+`3Wvv)yTqU-RamaA1IvwmKeN5=CxU~$^n^Nk-RkV>KV z!X?f1o4k{&)%MHg_H?O!;{9hU=YF9ahGVt`>QHB*v3-I7G|=6TY9L@3dEYVze3eApNcvA@xX*3Vol zh$Kp*W8wvK3qbXXb;b`^5+LxTV{ak>8mo6&5X#Ej2Kc0tf~U9&CpU9&XQu6XdUk{t z53Oh8CFdcK7_yIhj~6)`xB6CacHs>EW=H=m)ue_yR4ISw{VZpn-)ZjGy)SFrk@V%$ zz8{c@BuLaISK0fK>bk9rCfr2j{Gpcm=qF?LenFzqdJhJ(qhxPX&PjspqKcU{#k=Rqbc(eVA0b zbS)kZCBUFOUKdh9;|f=RS@9Us(O}sWogc~P1C6Du>OsH)Y03lzkbJ3;L2@Sg{1}k- zfXo;1b4GJ!I+`e~@D!f{M5F99<9k5j0a{Sj}ufKmso&I|J@`99Z^l)MkO-KL_A z6HtrnYS_>$X8%;a?)KAF4}Z0%;_`GeRD>ZlD>3z+6q{<>{)X{(~G1=32| zHLLnW2txJ*hH`f*hgY(@tW}y=SE4)n`bmCSYcXhxAC?s#6hPPwdxUds`*Gol7~@mo z?YJ&m&uv3#Dt;i_e^EY3^StKA9*JMJO9T{R_y6#Mg@UARDiTT~=pE$(MJ zHFu$h2(FFQn5BEMEZ#ONVc8FBHDYM-La}e>v>*jtlcdrO70yS`XUzQ#1Jn6V5kHnL zwBmEhy0m@PkK0gA0TtVy_qEOiI~q;mqvz+Nv1oj(wDKtkDhyPBE7t4eQY2?!Rbmah>{+il6e8-rO%zaIH zsomuV?q~y6(@Fo$d7jZ;ARyQ&Qku@n<-<%2JS3Z^&ghQ#bh26hRrXExmE=H=wn#^g zE2Wp1Z{&iZCoLgHETrl{D-O8OC}f!pnZq5%Bfz zF&Li=p|a>2O@ZhQi@U>@lmpLtCv@tyL9St0c&uJcm;u}QLk?%9IRf&L8jpZP4M+C| zUYrehY4#Ew;6fYS7`i!Mrk_|QBfT_s$^Xp4(DE^CEnbgmb`4t%neFjG&7ah`g_>Pl z@SC>0KVw0$OV4caFMB+)g73bqjryn;p4Egvh?@g?40M= zx5h@NgiMiAbFy5E9rDMtJ6rEUS8d@mxg9&u50ii@vM&hAx9hiZ^W!pOT!0KIA#aiK z(uvI(1TPdmBPJbMef{7VIP??!yp32XOZN34M+XEc z%L<-KA7$fro$*c9Jjo0bpN35cF0ZWR`@J!L9&san3;mP00NWCsN3w7FQhFh?hK-^7 zi+jQy#Vgh(l8Ricu6KVtRq}dm?y9G z1w4fy8Z{*3$!FlL>>ILb05O{Q_>6rx?)fmGQ*Nf8|ND&GN?baSI6yb364x%Fd0MS% zrP<=)(&`YVE}9ugbhdCOsBiZ9>ji3c@U`6nmg)I% zAI_-zjj!E6y9Q^)YqX>FWA@T_gO59{h4-rn;b?6@xi>@ z@ld=FFDRjtqRzck;@%KJ{=qLL7*pML>gB$jD)*W6xHtOs+%SWxs*oT(sKZx-lzIZR zQknE_C}G3T`?710gGZv@J0NX4ZR^psqK2*tKe+BoaZcVQ8=s`gv4SU#DH3lG^)sOd#E({7gGM-_ZBb)j|% z4I9o0ovvblO|_^~>!Lr8wo~5zS5;r`K-qGok})A5%NLQ!V5=_3D$|8}Z_x0kYGCci zip)Hao2ljxe&)q&+GMDgO>)^U2BAjEVox0GY2e7hlC3xB%n=@gRd1Mn-{Cctt^1#6 zZ$!vr7294A>r(AB<;ysv@II4@n)29FHZN#L9&??fjwt7stiu}v=V5f8Mjj_ADe+KGyGuW_gc%mjTqu7xnsA$+~37()p)RznGGnMTtEcmZ$X0=-&fqF%VB9uaGXPZvAE0SP0O;QA_hMV$n5;cc7l}ki``O&GLl>^%k#>O zGGBM+C_E}#MhC5O%1+bAf27dF?ue0H_Anp8ToaRQb$Ku}GGl@xki-HSTfGI90687jg~;{0q?XLRV743ILT5vlu00 zWPG3o232uBVExb68ADT;>;B6X)&Iat&j%d|0-y8VbXkLF(tTi=Y!o%52N2{T!D+x_ zI?r14QY-Bk+iyf=U(-`8K&;mdmH)BR=%(MjerhMNCJH17XENB;NK z{J&oMpK3+vl#^rbfbylc%Uw$WHGf95m&vyuOfXNO? z!Ep=6E^*c!oL*=l*nm0M2f|<%&g~|EMS}!7eoo8k=+CK@N@VqK*kb@Qu72itBia&* z_&BY_45PDpgKazugy#PMIY_ikP4N#TLlvuLr&f2U7vd$5mK|jr3Mna*)j-Nav)TGj zKZ@%sCC$5Py}c)xPg)rT!pJ&nFEO){I>*;R-&eD=@v~`}UbW3uk{9wtUs}}QEF!dL z8!`%Iv2UFamjMm=gAWi>yCJ_lSiZA4xeFptp;EZVyu6jQuikc>t3Na;D%?oWUY(pG zd6zwi9F-fH%aZ_7fO;X7AOI?WjH@-xMp`jS#0fAS<=k!_1fLd=EBZC)xy)V}q&Do~ zareP8Z{FUZj3Jyq>==C@Jf-*^cusA-A2+sH#01j^@L_iM)}I1)-wLy;8H=3mMTX-@ z4gkOW#HZIE|z8ns(Gr_H;*)C%WEe7*z0m_cCO z7_@nd$SstmVNwUu#f(7>(1QE&fcuw@wcOwbASPa(j{O+%-5K*$4UKLH0P)$)Fko!0 z8Pg`=Fp88xZ^-8A%3XIiq>?8AIW&J&Nyicg8~NfDpu)*xr^ zs13;^=bd#Gh@V?uSg0-;?B~eylZ1eob5olccY8&X@vg=l6Y6r-dd}GXNF~ypU;w=C z_z%@<<)#jgus&*BpoQEsErHB!83OR@oAE_Hap7>oM4yp|fK=gqe&mpZynN)ibatCZmgDj>uVD=u<2I+4D5QI1691 zeu*6jOn7JX&_T+8_zjiEd38|wewwW8={UREvWmlaall1^@r=%`>kniWh8f##)ymVZ zV;+{woH_2GGSAp-aghW9hM1RfEJRg_YpaClieB$AM)-NVV|cMK`nydbh;KwAMQ5nrySWpFvg9UB@ z3BQ|DB95p}83lVlwi3JglTSc%F7tox+xn7{spB;sM2)UiKCL~@__fGCKX{MjZ_g-&#z#I$yx8#4sE z)H6#*xL&V!G&|d^+}O?*@lDjyH?R2+&23-k2*Kq`0RS`}d6~UcZ4dQv*)b=C0}LWi zb}Gi;?6JN)_bfu|eNWiNm04xll$eoRHyyd9X`zD2Ja;0g%WH zMl7MO-B$WZO1?LY$Z)O75^N)tXE#3J*pEq{S1q^XuaBB+V8iv_I@#j#fgCwQnYXhL zsR4Hg?*2(NoE9i2;;Za?M&5F)C3BqVqVIu#X(LN_?bM7J)ZoNZ|`McG_mnnh(4s<(a-WqK;xpZ@Uk+4yM;sYSitql`<6oL0zl zf@!W1YkO+;uro|-u5Wpg9l?^Brf`i7iBQLR`Q&p;j>;yv4XQEmZu98{%&zR2RHD~v z@8nFM&dw{=S}@NhmMdsGaU)S-azb&^V3NoX;MMiKqp$CMS?&3AGxw3&-58zibk4#k zsr|` zHjCR&+!7w-g`0J0`rOhHeKcX`bNlAl$GMqi6u+a-c}4oe8>=roRUE2g#d5oZJY5kt ze!PiXPul-Y$WwIr;_7`M!!Ai_+|$+ao*8%zU+~29In*Jk=4*u*4qM+b7IL}aj?qK7 zK)mHtv)Uy%V_M}08eRUl9tFnp%*>~kjUM!QY}7vyskk%e_zUSdD;u+PX5@Mz{b4+6 z3b}5s-`$+^=7bRWF1LNStp3K#GEvkp@gw@kp{}eYHKu!(;9yP8)kKeNLE!=sPrQNl z#VYPu^k*-ReQA8#JCb2eqgGN+;^%W3KB~Pf*@KVb zBjMp%qX)OGU5y#ujE(nHK8-^^Hl3nBYzaSm)FP?KYWYd%nDOH~=3U7XzKCloks*of zU75|~KQRvsu~Yb6^B zaW&rjrxtIdG}~?oc_0yVor?afkv<9e z>fyMvXljYMX~{h>-25fQvq+{p+1c6i?9o(P69ED+My~08RV} zxhav6ITS8s$G0FVsZ@@5AfWB9-mFT%5SLUb;a{}HA#a0aLRj;x?vz7 z2M4>dX6(bUdEL|dodtIBBsygvd2CdM#d^+g$pgil&_6sf{PcBEae&Nd|AG9$Fdjvx zB_ekzZM{bhU70)S#C1Zk#!t$6o^SGKPIgS~NBpw}x=#EPIyKbb-mofug_x<%B!UmFa&270W7KYLAFVQE{cF%jNT>mlE%mCodHB`yc*Ll8aBeOlDYMs=vw?94H@A{C=D4Q5um4RCLK$CmCw)x z?QSZybv&*aXr}LJ*s63cq|0l~`A#wi;~iozkU_=zUlojyMasbh(I zoiSU^Pc8EB>n{<6t`nE|DLTu6ohNAaCk;=rxi6-kq<47k@+nPzLWse-cGB$FJ&)BV zbnd}BE%b*4jDTrVZZDb7Q1f8US;cKcEvy50wdq4J!QR?Q_twQg2C6^Ol5-&Gv=X6lT{e(cgQ~8spQ6Yl>AI}(>0Lubwr9)3x~IV;%<|Qno_BmmG<&63oP+{V^t=&5Nxj z3O8#q5nL{unRZdo3h1$H1OWnJH6Q>P8=Pn(@%128SlIDeEc?`GL>i!@8Yu0R=_E`mYpL&_MF!IU6nNAQi^|;=W8_I&r%CJAQk_h;}PxjYFxQ4kV>`zN7Z3 zB2b>ZuiXTGs0%c`{iX24~>$u2##R zuc}4iZO@*r-nP15<@D;R3npx}))0Pm+X3AkeVC^Zkv7iSHh+^WQp#cLC&$K^3`8eM zpHtXeV3A^4WRyoGo=#2;m=&dNyz3R?Q1VW8u_5=j z^rf-n4s?*r!OImN4G8^dn=m)>^v<1$$#F}w>SUT2yR^4Ry1&0Fx~x5J{`gvX8^Nn* zzh*Dm^MM!dr~R$2{Upno5_?kH`c$;)+1JZ2`qI`1*Qbb2=q|QCKL`{z{tj(FV&9_^ zNK+juuFvUPCuiDJ4OKsq_%1LJaC?xumb{w@VsO7JMVTQBP-7MN&rOrpt>>i8ogLskduwx8ZG zzX5@61~y^TX1`)nm~SR^2()kHF83JY4BoAFx*Z%aE||r6ULF_;JS&#l;LR^lRp^%9 zS65VDIiGoFDsA#Wl1hC4LUIAr*4Q4^GJSC{$6`!ZloS7zTh%iOT^QDvDV6fEb z_ujN2u>nwcSwq5SkYosz-DfJzAj#puGodS|7Vc0Cj)0O+8AT$4pA69Mj3WU5hs{4C zWT8W8Lwf@iXoi9QN0Ls%bCz4X6l%$2i9diOqV%M0xIvmSyx!`>a^Q@jxF3+wE`weG zbw53FXi@|6=G=jj8F2+0f$KnLfCfPCo*X%}I>>tIz{v1~p%Oi{S_I1sRKIX$zvQ!3 z8F5Y5sO^eYXTu_Eyk8N`ISR<@`X6+p`pW|G@327Z>mjUTgQ)|}fLwtgDSSa(cZ&H4dUkB-5-zi=41?`OrdI{ z9)1F*t>*sa5GQ^xI}(FH*vKK-!E4&vjFYUu9f8qdz#CDBm+=^{*&FWTum5oQO8|we zQOubacjp-=H)%eLUucbQh#)ky7rLRrgJ)^(iFLp(+0N6rzge|1neniH5<$7NliAg0 z9P3{9OeXH#-No|G%!lD$Y_(W3P^;{;lF0R=@oZn#)yN literal 0 HcmV?d00001 diff --git a/docs/tutorials/cli-images/classes_02.png b/docs/tutorials/cli-images/classes_02.png new file mode 100644 index 0000000000000000000000000000000000000000..f768d1877431ef84e6c80fff357bf772260ca8bd GIT binary patch literal 38614 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 YYp?(OhMg2!3`4kT%GyfU+ZI3n4`_. + +What are we going to build? +--------------------------- + +We will build a CLI application that helps to search for the movies. Let's call it Movie Lister. + +How does Movie Lister work? + +- There is a movies database +- Each movie has next fields: + - Title + - Year of the release + - Director's name +- The database is distributed in two formats: + - Csv + - Sqlite +- Application uses the movies database to search for the movies +- Application can search for the movies by: + - Director's name + - Year of the release +- Other database formats can be added later + +Movie Lister is a naive example from Martin Fowler's article about the dependency injection and +inversion of control: + + http://www.martinfowler.com/articles/injection.html + +Here is a class diagram of the Movie Lister application: + +.. image:: cli-images/classes_01.png + +The responsibilities are split next way: + +- ``MovieLister`` - is responsible for the search +- ``MovieFinder`` - is responsible for the fetching from the database +- ``Movie`` - the movie entity + +Prepare the environment +----------------------- + +Let's create the environment for the project. + +First we need to create a project folder and the virtual environment: + +.. code-block:: bash + + mkdir movie-lister-tutorial + cd movie-lister-tutorial + python3 -m venv venv + +Now let's activate the virtual environment: + +.. code-block:: bash + + . venv/bin/activate + +Project layout +-------------- + +Create next structure in the project root directory. All files are empty. That's ok for now. + +Initial project layout: + +.. code-block:: bash + + ./ + ├── movies/ + │ ├── __init__.py + │ ├── __main__.py + │ └── containers.py + ├── venv/ + ├── config.yml + └── requirements.txt + +Move on to the project requirements. + +Install the requirements +------------------------ + +Now it's time to install the project requirements. We will use next packages: + +- ``dependency-injector`` - the dependency injection framework +- ``pyyaml`` - the YAML files parsing library, used for the reading of the configuration files +- ``pytest`` - the test framework +- ``pytest-cov`` - the helper library for measuring the test coverage + +Put next lines into the ``requirements.txt`` file: + +.. code-block:: bash + + dependency-injector + pyyaml + pytest + pytest-cov + +and run next in the terminal: + +.. code-block:: bash + + pip install -r requirements.txt + +The requirements are setup. Now we will add the fixtures. + +Fixtures +-------- + +In this section we will add the fixtures. + +We will create a script that creates database files. + +First add the folder ``data/`` in the root of the project and then add the file +``fixtures.py`` inside of it: + +.. code-block:: bash + :emphasize-lines: 2-3 + + ./ + ├── data/ + │ └── fixtures.py + ├── movies/ + │ ├── __init__.py + │ ├── __main__.py + │ └── containers.py + ├── venv/ + ├── config.yml + └── requirements.txt + +Second put next in the ``fixtures.py``: + +.. code-block:: python + + """Fixtures module.""" + + import csv + import sqlite3 + import pathlib + + + SAMPLE_DATA = [ + ('The Hunger Games: Mockingjay - Part 2', 2015, 'Francis Lawrence'), + ('Rogue One: A Star Wars Story', 2016, 'Gareth Edwards'), + ('The Jungle Book', 2016, 'Jon Favreau'), + ] + + FILE = pathlib.Path(__file__) + DIR = FILE.parent + CSV_FILE = DIR / 'movies.csv' + SQLITE_FILE = DIR / 'movies.db' + + + def create_csv(movies_data, path): + with open(path, 'w') as opened_file: + writer = csv.writer(opened_file) + for row in movies_data: + writer.writerow(row) + + + def create_sqlite(movies_data, path): + with sqlite3.connect(path) as db: + db.execute( + 'CREATE TABLE IF NOT EXISTS movies ' + '(title text, year int, director text)' + ) + db.execute('DELETE FROM movies') + db.executemany('INSERT INTO movies VALUES (?,?,?)', movies_data) + + + def main(): + create_csv(SAMPLE_DATA, CSV_FILE) + create_sqlite(SAMPLE_DATA, SQLITE_FILE) + print('OK') + + + if __name__ == '__main__': + main() + +Now run in the terminal: + +.. code-block:: bash + + python data/fixtures.py + +You should see: + +.. code-block:: bash + + OK + +Check that files ``movies.csv`` and ``movies.db`` have appeared in the ``data/`` folder: + +.. code-block:: bash + :emphasize-lines: 4-5 + + ./ + ├── data/ + │ ├── fixtures.py + │ ├── movies.csv + │ └── movies.db + ├── movies/ + │ ├── __init__.py + │ ├── __main__.py + │ └── containers.py + ├── venv/ + ├── config.yml + └── requirements.txt + +Fixtures are created. Let's move on. + +Container +--------- + +In this section we will add the main part of our application - the container. + +Container will keep all of the application components and their dependencies. + +Edit ``containers.py``: + +.. code-block:: python + + """Containers module.""" + + from dependency_injector import containers + + + class ApplicationContainer(containers.DeclarativeContainer): + ... + +Container is empty for now. We will add the providers in the following sections. + +Let's also create the ``main()`` function. Its responsibility is to run our application. For now +it will just create the container. + +Edit ``__main__.py``: + +.. code-block:: python + + """Main module.""" + + from .containers import ApplicationContainer + + + def main(): + container = ApplicationContainer() + + + if __name__ == '__main__': + main() + +.. note:: + + Container is the first object in the application. + + The container is used to create all other objects. + +Csv finder +---------- + +In this section we will build everything we need for working with the csv file formats. + +We will add: + +- The ``Movie`` entity +- The ``MovieFinder`` base class +- The ``CsvMovieFinder`` finder implementation +- The ``MovieLister`` class + +After each step we will add the provider to the container. + +.. image:: cli-images/classes_02.png + +Create the ``entities.py`` in the ``movies`` package: + +.. code-block:: bash + :emphasize-lines: 10 + + ./ + ├── data/ + │ ├── fixtures.py + │ ├── movies.csv + │ └── movies.db + ├── movies/ + │ ├── __init__.py + │ ├── __main__.py + │ ├── containers.py + │ └── entities.py + ├── venv/ + ├── config.yml + └── requirements.txt + +and put next into it: + +.. code-block:: python + + """Movie entities module.""" + + + class Movie: + + def __init__(self, title: str, year: int, director: str): + self.title = str(title) + self.year = int(year) + self.director = str(director) + + def __repr__(self): + return '{0}(title={1}, year={2}, director={3})'.format( + self.__class__.__name__, + repr(self.title), + repr(self.year), + repr(self.director), + ) + +Now we need to add the ``Movie`` factory to the container. We need to add import of the +``providers`` module from the ``dependency_injector`` package, import ``entities`` module. + +Edit ``containers.py``: + +.. code-block:: python + :emphasize-lines: 3,5,9 + + """Containers module.""" + + from dependency_injector import containers, providers + + from . import entities + + class ApplicationContainer(containers.DeclarativeContainer): + + movie = providers.Factory(entities.Movie) + +.. note:: + + Don't forget to remove the Ellipsis ``...`` from the container. We don't need it anymore + since we container is not empty. + +Let's move on to the finders. + +Create the ``finders.py`` in the ``movies`` package: + +.. code-block:: bash + :emphasize-lines: 11 + + ./ + ├── data/ + │ ├── fixtures.py + │ ├── movies.csv + │ └── movies.db + ├── movies/ + │ ├── __init__.py + │ ├── __main__.py + │ ├── containers.py + │ ├── entities.py + │ └── finders.py + ├── venv/ + ├── config.yml + └── requirements.txt + +and put next into it: + +.. code-block:: python + + """Movie finders module.""" + + import csv + from typing import Callable, List + + from .entities import Movie + + + class MovieFinder: + + def __init__(self, movie_factory: Callable[..., Movie]) -> None: + self._movie_factory = movie_factory + + def find_all(self) -> List[Movie]: + raise NotImplementedError() + + + class CsvMovieFinder(MovieFinder): + + def __init__( + self, + movie_factory: Callable[..., Movie], + path: str, + delimiter: str, + ) -> None: + self._csv_file_path = path + self._delimiter = delimiter + super().__init__(movie_factory) + + def find_all(self) -> List[Movie]: + with open(self._csv_file_path) as csv_file: + csv_reader = csv.reader(csv_file, delimiter=self._delimiter) + return [self._movie_factory(*row) for row in csv_reader] + +Now let's add the csv finder into the container. + +Edit ``containers.py``: + +.. code-block:: python + :emphasize-lines: 5,9,13-18 + + """Containers module.""" + + from dependency_injector import containers, providers + + from . import finders, entities + + class ApplicationContainer(containers.DeclarativeContainer): + + config = providers.Configuration() + + movie = providers.Factory(entities.Movie) + + csv_finder = providers.Singleton( + finders.CsvMovieFinder, + movie_factory=movie.provider, + path=config.finder.csv.path, + delimiter=config.finder.csv.delimiter, + ) + +The csv finder needs the movie factory. It needs it to create the ``Movie`` entities when +reads the csv rows. To provide the factory we use ``.provider`` factory attribute. +This is also called the delegation of the provider. If we just pass the movie factory +as the dependency, it will be called when csv finder is created and the ``Movie`` instance will +be injected. With the ``.provider`` attribute the provider itself will be injected. + +The csv finder also has a few dependencies on the configuration options. We added configuration +provider to provide these dependencies. + +.. note:: + + We have used the configuration value before it was defined. That's the principle how the + Configuration provider works. + + Use first, define later. + +Not let's define the configuration values. + +Edit ``config.yml``: + +.. code-block:: yaml + + finder: + + csv: + path: "data/movies.csv" + delimiter: "," + +The configuration file is ready. Now let's update the ``main()`` function to specify its location. + +Edit ``__main__.py``: + +.. code-block:: python + :emphasize-lines: 9 + + """Main module.""" + + from .containers import ApplicationContainer + + + def main(): + container = ApplicationContainer() + + container.config.from_yaml('config.yml') + + + if __name__ == '__main__': + main() + +Move on to the lister. + +Create the ``listers.py`` in the ``movies`` package: + +.. code-block:: bash + :emphasize-lines: 12 + + ./ + ├── data/ + │ ├── fixtures.py + │ ├── movies.csv + │ └── movies.db + ├── movies/ + │ ├── __init__.py + │ ├── __main__.py + │ ├── containers.py + │ ├── entities.py + │ ├── finders.py + │ └── listers.py + ├── venv/ + ├── config.yml + └── requirements.txt + +and put next into it: + +.. code-block:: python + + """Movie listers module.""" + + from .finders import MovieFinder + + + class MovieLister: + + def __init__(self, movie_finder: MovieFinder): + self._movie_finder = movie_finder + + def movies_directed_by(self, director): + return [ + movie for movie in self._movie_finder.find_all() + if movie.director == director + ] + + def movies_released_in(self, year): + return [ + movie for movie in self._movie_finder.find_all() + if movie.year == year + ] + +and edit ``containers.py``: + +.. code-block:: python + :emphasize-lines: 5,20-23 + + """Containers module.""" + + from dependency_injector import containers, providers + + from . import finders, listers, entities + + class ApplicationContainer(containers.DeclarativeContainer): + + config = providers.Configuration() + + movie = providers.Factory(entities.Movie) + + csv_finder = providers.Singleton( + finders.CsvMovieFinder, + movie_factory=movie.provider, + path=config.finder.csv.path, + delimiter=config.finder.csv.delimiter, + ) + + lister = providers.Factory( + listers.MovieLister, + movie_finder=csv_finder, + ) + +All the components are created and added to the container. + +Finally let's update the ``main()`` function. + +Edit ``__main__.py``: + +.. code-block:: python + :emphasize-lines: 11-20 + + """Main module.""" + + from .containers import ApplicationContainer + + + def main(): + container = ApplicationContainer() + + container.config.from_yaml('config.yml') + + lister = container.lister() + + print( + 'Francis Lawrence movies:', + lister.movies_directed_by('Francis Lawrence'), + ) + print( + '2016 movies:', + lister.movies_released_in(2016), + ) + + + if __name__ == '__main__': + main() + +All set. Now we run the application. + +Run in the terminal: + +.. code-block:: bash + + python -m movies + +You should see: + +.. code-block:: bash + + Francis Lawrence movies: [Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')] + 2016 movies: [Movie(title='Rogue One: A Star Wars Story', year=2016, director='Gareth Edwards'), Movie(title='The Jungle Book', year=2016, director='Jon Favreau')] + +Our application can work with the movies database in the csv format. We also need to support +the sqlite format. We will deal with it in the next section. + +Sqlite finder +------------- + +In this section we will add another type of the finder - the sqlite finder. + +Let's get to work. + +Edit ``finders.py``: + +.. code-block:: python + :emphasize-lines: 4,37-50 + + """Movie finders module.""" + + import csv + import sqlite3 + from typing import Callable, List + + from .entities import Movie + + + class MovieFinder: + + def __init__(self, movie_factory: Callable[..., Movie]) -> None: + self._movie_factory = movie_factory + + def find_all(self) -> List[Movie]: + raise NotImplementedError() + + + class CsvMovieFinder(MovieFinder): + + def __init__( + self, + movie_factory: Callable[..., Movie], + path: str, + delimiter: str, + ) -> None: + self._csv_file_path = path + self._delimiter = delimiter + super().__init__(movie_factory) + + def find_all(self) -> List[Movie]: + with open(self._csv_file_path) as csv_file: + csv_reader = csv.reader(csv_file, delimiter=self._delimiter) + return [self._movie_factory(*row) for row in csv_reader] + + + class SqliteMovieFinder(MovieFinder): + + def __init__( + self, + movie_factory: Callable[..., Movie], + path: str, + ) -> None: + self._database = sqlite3.connect(path) + super().__init__(movie_factory) + + def find_all(self) -> List[Movie]: + with self._database as db: + rows = db.execute('SELECT title, year, director FROM movies') + return [self._movie_factory(*row) for row in rows] + +Now we need to add the sqlite finder to the container and update lister's dependency to use it. + +Edit ``containers.py``: + +.. code-block:: python + :emphasize-lines: 20-24,28 + + """Containers module.""" + + from dependency_injector import containers, providers + + from . import finders, listers, entities + + class ApplicationContainer(containers.DeclarativeContainer): + + config = providers.Configuration() + + movie = providers.Factory(entities.Movie) + + csv_finder = providers.Singleton( + finders.CsvMovieFinder, + movie_factory=movie.provider, + path=config.finder.csv.path, + delimiter=config.finder.csv.delimiter, + ) + + sqlite_finder = providers.Singleton( + finders.SqliteMovieFinder, + movie_factory=movie.provider, + path=config.finder.sqlite.path, + ) + + lister = providers.Factory( + listers.MovieLister, + movie_finder=sqlite_finder, + ) + +The sqlite finder has a dependency on the configuration option. Let's update the configuration +file. + +Edit ``config.yml``: + +.. code-block:: yaml + :emphasize-lines: 7-8 + + finder: + + csv: + path: "data/movies.csv" + delimiter: "," + + sqlite: + path: "data/movies.db" + +All is ready. Let's check. + +Run in the terminal: + +.. code-block:: bash + + python -m movies + +You should see: + +.. code-block:: bash + + Francis Lawrence movies: [Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')] + 2016 movies: [Movie(title='Rogue One: A Star Wars Story', year=2016, director='Gareth Edwards'), Movie(title='The Jungle Book', year=2016, director='Jon Favreau')] + +Our application now supports both formats: csv files and sqlite databases. Every time when we +need to work with the different format we need to make a code change in the container. We will +improve this in the next section. + +Selector +-------- + +In this section we will make our application more flexible. + +The code change will not be needed to switch between csv and sqlite formats. We implement the +switch based on the environment variable ``MOVIE_FINDER_TYPE``: + +- When ``MOVIE_FINDER_TYPE=csv`` application uses csv finder. +- When ``MOVIE_FINDER_TYPE=sqlite`` application uses sqlite finder. + +We will use the ``Selector`` provider. It selects the provider based on the configuration option +(docs - :ref:`selector-provider`). + +Edit ``containers.py``: + +.. code-block:: python + :emphasize-lines: 27-31,35 + + """Containers module.""" + + from dependency_injector import containers, providers + + from . import finders, listers, entities + + + class ApplicationContainer(containers.DeclarativeContainer): + + config = providers.Configuration() + + movie = providers.Factory(entities.Movie) + + csv_finder = providers.Singleton( + finders.CsvMovieFinder, + movie_factory=movie.provider, + path=config.finder.csv.path, + delimiter=config.finder.csv.delimiter, + ) + + sqlite_finder = providers.Singleton( + finders.SqliteMovieFinder, + movie_factory=movie.provider, + path=config.finder.sqlite.path, + ) + + finder = providers.Selector( + config.finder.type, + csv=csv_finder, + sqlite=sqlite_finder, + ) + + lister = providers.Factory( + listers.MovieLister, + movie_finder=finder, + ) + +The switch is the ``config.finder.type`` option. When its value is ``csv``, the provider under +``csv`` key is used. The same is for ``sqlite``. + +Now we need to read the value of the ``config.finder.type`` option from the environment variable +``MOVIE_FINDER_TYPE``. + +Edit ``__main__.py``: + +.. code-block:: python + :emphasize-lines: 10 + + """Main module.""" + + from .containers import ApplicationContainer + + + def main(): + container = ApplicationContainer() + + container.config.from_yaml('config.yml') + container.config.finder.type.from_env('MOVIE_FINDER_TYPE') + + lister = container.lister() + + print( + 'Francis Lawrence movies:', + lister.movies_directed_by('Francis Lawrence'), + ) + print( + '2016 movies:', + lister.movies_released_in(2016), + ) + + + if __name__ == '__main__': + main() + +Done. + +Run in the terminal line by line: + +.. code-block:: bash + + MOVIE_FINDER_TYPE=csv python -m movies + MOVIE_FINDER_TYPE=sqlite python -m movies + +The output should be something like this for each command: + +.. code-block:: bash + + Francis Lawrence movies: [Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')] + 2016 movies: [Movie(title='Rogue One: A Star Wars Story', year=2016, director='Gareth Edwards'), Movie(title='The Jungle Book', year=2016, director='Jon Favreau')] + +In the next section we will add some tests. + +Tests +----- + +It would be nice to add some tests. Let's do it. + +We will use `pytest `_ and +`coverage `_. + +Create ``tests.py`` in the ``movies`` package: + +.. code-block:: bash + :emphasize-lines: 13 + + ./ + ├── data/ + │ ├── fixtures.py + │ ├── movies.csv + │ └── movies.db + ├── movies/ + │ ├── __init__.py + │ ├── __main__.py + │ ├── containers.py + │ ├── entities.py + │ ├── finders.py + │ ├── listers.py + │ └── tests.py + ├── venv/ + ├── config.yml + └── requirements.txt + +and put next into it: + +.. code-block:: python + :emphasize-lines: 35,50 + + """Tests module.""" + + from unittest import mock + + import pytest + + from .containers import ApplicationContainer + + + @pytest.fixture + def container(): + container = ApplicationContainer() + container.config.from_dict({ + 'finder': { + 'type': 'csv', + 'csv': { + 'path': '/fake-movies.csv', + 'delimiter': ',', + }, + 'sqlite': { + 'path': '/fake-movies.db', + }, + }, + }) + return container + + + def test_movies_directed_by(container): + finder_mock = mock.Mock() + finder_mock.find_all.return_value = [ + container.movie('The 33', 2015, 'Patricia Riggen'), + container.movie('The Jungle Book', 2016, 'Jon Favreau'), + ] + + with container.finder.override(finder_mock): + lister = container.lister() + movies = lister.movies_directed_by('Jon Favreau') + + assert len(movies) == 1 + assert movies[0].title == 'The Jungle Book' + + + def test_movies_released_in(container): + finder_mock = mock.Mock() + finder_mock.find_all.return_value = [ + container.movie('The 33', 2015, 'Patricia Riggen'), + container.movie('The Jungle Book', 2016, 'Jon Favreau'), + ] + + with container.finder.override(finder_mock): + lister = container.lister() + movies = lister.movies_released_in(2015) + + assert len(movies) == 1 + assert movies[0].title == 'The 33' + +Run in the terminal: + +.. code-block:: bash + + pytest movies/tests.py --cov=movies + +You should see: + +.. code-block:: bash + + platform darwin -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1 + plugins: cov-2.10.0 + collected 2 items + + movies/tests.py .. [100%] + + ---------- coverage: platform darwin, python 3.8.3-final-0 ----------- + Name Stmts Miss Cover + ------------------------------------------ + movies/__init__.py 0 0 100% + movies/__main__.py 10 10 0% + movies/containers.py 9 0 100% + movies/entities.py 7 1 86% + movies/finders.py 26 13 50% + movies/listers.py 8 0 100% + movies/tests.py 24 0 100% + ------------------------------------------ + TOTAL 84 24 71% + +.. note:: + + Take a look at the highlights in the ``tests.py``. + + We use ``.override()`` method of the ``finder`` provider. Provider is overridden by the mock. + Every time when any other provider will request ``finder`` provider to provide the dependency, + the mock will be returned. So when we call the ``lister`` provider, the ``MovieLister`` + instance is created with the mock, not an actual ``MovieFinder``. + +Conclusion +---------- + +In this tutorial we've built a CLI application following the dependency injection principle. +We've used the ``Dependency Injector`` as a dependency injection framework. + +The benefit you get with the ``Dependency Injector`` is the container. It starts to payoff +when you need to understand or change your application structure. It's easy with the container, +cause you have everything defined explicitly in one place: + +.. code-block:: python + + """Containers module.""" + + from dependency_injector import containers, providers + + from . import finders, listers, entities + + + class ApplicationContainer(containers.DeclarativeContainer): + + config = providers.Configuration() + + movie = providers.Factory(entities.Movie) + + csv_finder = providers.Singleton( + finders.CsvMovieFinder, + movie_factory=movie.provider, + path=config.finder.csv.path, + delimiter=config.finder.csv.delimiter, + ) + + sqlite_finder = providers.Singleton( + finders.SqliteMovieFinder, + movie_factory=movie.provider, + path=config.finder.sqlite.path, + ) + + finder = providers.Selector( + config.finder.type, + csv=csv_finder, + sqlite=sqlite_finder, + ) + + lister = providers.Factory( + listers.MovieLister, + movie_finder=finder, + ) + +What's next? + +- Look at the other :ref:`tutorials` +- Know more about the :ref:`providers` +- Go to the :ref:`contents` + +.. disqus:: diff --git a/docs/tutorials/flask.rst b/docs/tutorials/flask.rst index af3a222e..e36b17c9 100644 --- a/docs/tutorials/flask.rst +++ b/docs/tutorials/flask.rst @@ -1097,7 +1097,7 @@ In this tutorial we've built a ``Flask`` application following the dependency in We've used the ``Dependency Injector`` as a dependency injection framework. The main part of this application is the container. It keeps all the application components and -their dependencies in one place: +their dependencies defined explicitly in one place: .. code-block:: python @@ -1141,9 +1141,9 @@ their dependencies in one place: What's next? -- Look at the other :ref:`tutorials`. -- Know more about the :ref:`providers`. -- Go to the :ref:`contents`. +- Look at the other :ref:`tutorials` +- Know more about the :ref:`providers` +- Go to the :ref:`contents` .. disqus:: diff --git a/docs/tutorials/index.rst b/docs/tutorials/index.rst index bfe6a2c2..f686fe4f 100644 --- a/docs/tutorials/index.rst +++ b/docs/tutorials/index.rst @@ -12,5 +12,6 @@ frameworks. flask aiohttp asyncio-daemon + cli .. disqus:: diff --git a/examples/.pydocstylerc b/examples/.pydocstylerc index c5b7bc0f..4d4d1367 100644 --- a/examples/.pydocstylerc +++ b/examples/.pydocstylerc @@ -1,2 +1,2 @@ [pydocstyle] -ignore = D100,D101,D102,D103,D107,D203,D213 +ignore = D100,D101,D102,D103,D105,D107,D203,D213 diff --git a/examples/miniapps/movie-lister/README.rst b/examples/miniapps/movie-lister/README.rst new file mode 100644 index 00000000..54a8e3a1 --- /dev/null +++ b/examples/miniapps/movie-lister/README.rst @@ -0,0 +1,75 @@ +Movie lister - a naive example of dependency injection in Python +================================================================ + +This is a Python implementation of the dependency injection example from Martin Fowler's +article: + + http://www.martinfowler.com/articles/injection.html + +Run +--- + +Create a virtual environment: + +.. code-block:: bash + + virtualenv venv + . venv/bin/activate + +Install the requirements: + +.. code-block:: bash + + pip install -r requirements.txt + +To create the fixtures do: + +.. code-block:: bash + + python data/fixtures.py + +To run the application do: + +.. code-block:: bash + + MOVIE_FINDER_TYPE=csv python -m movies + MOVIE_FINDER_TYPE=sqlite python -m movies + +The output should be something like this for each command: + +.. code-block:: bash + + Francis Lawrence movies: [Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')] + 2016 movies: [Movie(title='Rogue One: A Star Wars Story', year=2016, director='Gareth Edwards'), Movie(title='The Jungle Book', year=2016, director='Jon Favreau')] + +Test +---- + +To run the tests do: + +.. code-block:: bash + + pytest movies/tests.py --cov=movies + +The output should be something like: + +.. code-block:: + + platform darwin -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1 + plugins: cov-2.10.0 + collected 2 items + + movies/tests.py .. [100%] + + ---------- coverage: platform darwin, python 3.8.3-final-0 ----------- + Name Stmts Miss Cover + ------------------------------------------ + movies/__init__.py 0 0 100% + movies/__main__.py 10 10 0% + movies/containers.py 9 0 100% + movies/entities.py 7 1 86% + movies/finders.py 26 13 50% + movies/listers.py 8 0 100% + movies/tests.py 24 0 100% + ------------------------------------------ + TOTAL 84 24 71% diff --git a/examples/miniapps/movie-lister/config.yml b/examples/miniapps/movie-lister/config.yml new file mode 100644 index 00000000..35421f1b --- /dev/null +++ b/examples/miniapps/movie-lister/config.yml @@ -0,0 +1,8 @@ +finder: + + csv: + path: "data/movies.csv" + delimiter: "," + + sqlite: + path: "data/movies.db" diff --git a/examples/miniapps/movie-lister/data/.gitignore b/examples/miniapps/movie-lister/data/.gitignore new file mode 100644 index 00000000..ba68614f --- /dev/null +++ b/examples/miniapps/movie-lister/data/.gitignore @@ -0,0 +1,6 @@ +# Everything +* + +# Except this file: +!.gitignore +!fixtures.py diff --git a/examples/miniapps/movie-lister/data/fixtures.py b/examples/miniapps/movie-lister/data/fixtures.py new file mode 100644 index 00000000..2870d04d --- /dev/null +++ b/examples/miniapps/movie-lister/data/fixtures.py @@ -0,0 +1,44 @@ +"""Fixtures module.""" + +import csv +import sqlite3 +import pathlib + + +SAMPLE_DATA = [ + ('The Hunger Games: Mockingjay - Part 2', 2015, 'Francis Lawrence'), + ('Rogue One: A Star Wars Story', 2016, 'Gareth Edwards'), + ('The Jungle Book', 2016, 'Jon Favreau'), +] + +FILE = pathlib.Path(__file__) +DIR = FILE.parent +CSV_FILE = DIR / 'movies.csv' +SQLITE_FILE = DIR / 'movies.db' + + +def create_csv(movies_data, path): + with open(path, 'w') as opened_file: + writer = csv.writer(opened_file) + for row in movies_data: + writer.writerow(row) + + +def create_sqlite(movies_data, path): + with sqlite3.connect(path) as db: + db.execute( + 'CREATE TABLE IF NOT EXISTS movies ' + '(title text, year int, director text)' + ) + db.execute('DELETE FROM movies') + db.executemany('INSERT INTO movies VALUES (?,?,?)', movies_data) + + +def main(): + create_csv(SAMPLE_DATA, CSV_FILE) + create_sqlite(SAMPLE_DATA, SQLITE_FILE) + print('OK') + + +if __name__ == '__main__': + main() diff --git a/examples/miniapps/movie-lister/movies/__init__.py b/examples/miniapps/movie-lister/movies/__init__.py new file mode 100644 index 00000000..1c744ca5 --- /dev/null +++ b/examples/miniapps/movie-lister/movies/__init__.py @@ -0,0 +1 @@ +"""Top-level package.""" diff --git a/examples/miniapps/movie-lister/movies/__main__.py b/examples/miniapps/movie-lister/movies/__main__.py new file mode 100644 index 00000000..b51c4010 --- /dev/null +++ b/examples/miniapps/movie-lister/movies/__main__.py @@ -0,0 +1,25 @@ +"""Main module.""" + +from .containers import ApplicationContainer + + +def main(): + container = ApplicationContainer() + + container.config.from_yaml('config.yml') + container.config.finder.type.from_env('MOVIE_FINDER_TYPE') + + lister = container.lister() + + print( + 'Francis Lawrence movies:', + lister.movies_directed_by('Francis Lawrence'), + ) + print( + '2016 movies:', + lister.movies_released_in(2016), + ) + + +if __name__ == '__main__': + main() diff --git a/examples/miniapps/movie-lister/movies/containers.py b/examples/miniapps/movie-lister/movies/containers.py new file mode 100644 index 00000000..0ffd7f16 --- /dev/null +++ b/examples/miniapps/movie-lister/movies/containers.py @@ -0,0 +1,36 @@ +"""Containers module.""" + +from dependency_injector import containers, providers + +from . import finders, listers, entities + + +class ApplicationContainer(containers.DeclarativeContainer): + + config = providers.Configuration() + + movie = providers.Factory(entities.Movie) + + csv_finder = providers.Singleton( + finders.CsvMovieFinder, + movie_factory=movie.provider, + path=config.finder.csv.path, + delimiter=config.finder.csv.delimiter, + ) + + sqlite_finder = providers.Singleton( + finders.SqliteMovieFinder, + movie_factory=movie.provider, + path=config.finder.sqlite.path, + ) + + finder = providers.Selector( + config.finder.type, + csv=csv_finder, + sqlite=sqlite_finder, + ) + + lister = providers.Factory( + listers.MovieLister, + movie_finder=finder, + ) diff --git a/examples/miniapps/movie-lister/movies/entities.py b/examples/miniapps/movie-lister/movies/entities.py new file mode 100644 index 00000000..ccd27256 --- /dev/null +++ b/examples/miniapps/movie-lister/movies/entities.py @@ -0,0 +1,17 @@ +"""Movie entities module.""" + + +class Movie: + + def __init__(self, title: str, year: int, director: str): + self.title = str(title) + self.year = int(year) + self.director = str(director) + + def __repr__(self): + return '{0}(title={1}, year={2}, director={3})'.format( + self.__class__.__name__, + repr(self.title), + repr(self.year), + repr(self.director), + ) diff --git a/examples/miniapps/movie-lister/movies/finders.py b/examples/miniapps/movie-lister/movies/finders.py new file mode 100644 index 00000000..3485d8c3 --- /dev/null +++ b/examples/miniapps/movie-lister/movies/finders.py @@ -0,0 +1,50 @@ +"""Movie finders module.""" + +import csv +import sqlite3 +from typing import Callable, List + +from .entities import Movie + + +class MovieFinder: + + def __init__(self, movie_factory: Callable[..., Movie]) -> None: + self._movie_factory = movie_factory + + def find_all(self) -> List[Movie]: + raise NotImplementedError() + + +class CsvMovieFinder(MovieFinder): + + def __init__( + self, + movie_factory: Callable[..., Movie], + path: str, + delimiter: str, + ) -> None: + self._csv_file_path = path + self._delimiter = delimiter + super().__init__(movie_factory) + + def find_all(self) -> List[Movie]: + with open(self._csv_file_path) as csv_file: + csv_reader = csv.reader(csv_file, delimiter=self._delimiter) + return [self._movie_factory(*row) for row in csv_reader] + + +class SqliteMovieFinder(MovieFinder): + + def __init__( + self, + movie_factory: Callable[..., Movie], + path: str, + ) -> None: + self._database = sqlite3.connect(path) + super().__init__(movie_factory) + + def find_all(self) -> List[Movie]: + with self._database as db: + rows = db.execute('SELECT title, year, director FROM movies') + return [self._movie_factory(*row) for row in rows] diff --git a/examples/miniapps/movie-lister/movies/listers.py b/examples/miniapps/movie-lister/movies/listers.py new file mode 100644 index 00000000..36d254c2 --- /dev/null +++ b/examples/miniapps/movie-lister/movies/listers.py @@ -0,0 +1,21 @@ +"""Movie listers module.""" + +from .finders import MovieFinder + + +class MovieLister: + + def __init__(self, movie_finder: MovieFinder): + self._movie_finder = movie_finder + + def movies_directed_by(self, director): + return [ + movie for movie in self._movie_finder.find_all() + if movie.director == director + ] + + def movies_released_in(self, year): + return [ + movie for movie in self._movie_finder.find_all() + if movie.year == year + ] diff --git a/examples/miniapps/movie-lister/movies/tests.py b/examples/miniapps/movie-lister/movies/tests.py new file mode 100644 index 00000000..f6bfe81f --- /dev/null +++ b/examples/miniapps/movie-lister/movies/tests.py @@ -0,0 +1,55 @@ +"""Tests module.""" + +from unittest import mock + +import pytest + +from .containers import ApplicationContainer + + +@pytest.fixture +def container(): + container = ApplicationContainer() + container.config.from_dict({ + 'finder': { + 'type': 'csv', + 'csv': { + 'path': '/fake-movies.csv', + 'delimiter': ',', + }, + 'sqlite': { + 'path': '/fake-movies.db', + }, + }, + }) + return container + + +def test_movies_directed_by(container): + finder_mock = mock.Mock() + finder_mock.find_all.return_value = [ + container.movie('The 33', 2015, 'Patricia Riggen'), + container.movie('The Jungle Book', 2016, 'Jon Favreau'), + ] + + with container.finder.override(finder_mock): + lister = container.lister() + movies = lister.movies_directed_by('Jon Favreau') + + assert len(movies) == 1 + assert movies[0].title == 'The Jungle Book' + + +def test_movies_released_in(container): + finder_mock = mock.Mock() + finder_mock.find_all.return_value = [ + container.movie('The 33', 2015, 'Patricia Riggen'), + container.movie('The Jungle Book', 2016, 'Jon Favreau'), + ] + + with container.finder.override(finder_mock): + lister = container.lister() + movies = lister.movies_released_in(2015) + + assert len(movies) == 1 + assert movies[0].title == 'The 33' diff --git a/examples/miniapps/movie-lister/requirements.txt b/examples/miniapps/movie-lister/requirements.txt new file mode 100644 index 00000000..72ff627e --- /dev/null +++ b/examples/miniapps/movie-lister/requirements.txt @@ -0,0 +1,4 @@ +dependency-injector +pyyaml +pytest +pytest-cov diff --git a/examples/miniapps/movie_lister/README.rst b/examples/miniapps/movie_lister/README.rst deleted file mode 100644 index 616b9106..00000000 --- a/examples/miniapps/movie_lister/README.rst +++ /dev/null @@ -1,16 +0,0 @@ -A naive example of dependency injection in Python -================================================= - -Example implementation of dependency injection on Python from Martin Fowler's -article about dependency injection and inversion of control: - -http://www.martinfowler.com/articles/injection.html - - -Instructions for running: - -.. code-block:: bash - - python app_csv.py - python app_db.py - python app_db_csv.py diff --git a/examples/miniapps/movie_lister/app_csv.py b/examples/miniapps/movie_lister/app_csv.py deleted file mode 100644 index 052492bc..00000000 --- a/examples/miniapps/movie_lister/app_csv.py +++ /dev/null @@ -1,49 +0,0 @@ -"""A naive example of dependency injection on Python. - -Example implementation of dependency injection in Python from Martin Fowler's -article about dependency injection and inversion of control: - -http://www.martinfowler.com/articles/injection.html - -This mini application uses ``movies`` library, that is configured to work with -csv file movies database. -""" - -import movies -import movies.finders - -import example.db -import example.main - -import settings -import fixtures - -import dependency_injector.containers as containers -import dependency_injector.providers as providers - - -@containers.override(movies.MoviesModule) -class MyMoviesModule(containers.DeclarativeContainer): - """IoC container for overriding movies module component providers.""" - - finder = providers.Factory(movies.finders.CsvMovieFinder, - csv_file_path=settings.MOVIES_CSV_PATH, - delimiter=',', - **movies.MoviesModule.finder.kwargs) - - -class CsvApplication(containers.DeclarativeContainer): - """IoC container of csv application component providers.""" - - main = providers.Callable(example.main.main, - movie_lister=movies.MoviesModule.lister) - - init_db = providers.Callable(example.db.init_csv, - movies_data=fixtures.MOVIES_SAMPLE_DATA, - csv_file_path=settings.MOVIES_CSV_PATH, - delimiter=',') - - -if __name__ == '__main__': - CsvApplication.init_db() - CsvApplication.main() diff --git a/examples/miniapps/movie_lister/app_db.py b/examples/miniapps/movie_lister/app_db.py deleted file mode 100644 index dee6bdae..00000000 --- a/examples/miniapps/movie_lister/app_db.py +++ /dev/null @@ -1,55 +0,0 @@ -"""A naive example of dependency injection on Python. - -Example implementation of dependency injection in Python from Martin Fowler's -article about dependency injection and inversion of control: - -http://www.martinfowler.com/articles/injection.html - -This mini application uses ``movies`` library, that is configured to work with -sqlite movies database. -""" - -import sqlite3 - -import movies -import movies.finders - -import example.db -import example.main - -import settings -import fixtures - -import dependency_injector.containers as containers -import dependency_injector.providers as providers - - -class ResourcesModule(containers.DeclarativeContainer): - """IoC container of application resource providers.""" - - database = providers.Singleton(sqlite3.connect, settings.MOVIES_DB_PATH) - - -@containers.override(movies.MoviesModule) -class MyMoviesModule(containers.DeclarativeContainer): - """IoC container for overriding movies module component providers.""" - - finder = providers.Factory(movies.finders.SqliteMovieFinder, - database=ResourcesModule.database, - **movies.MoviesModule.finder.kwargs) - - -class DbApplication(containers.DeclarativeContainer): - """IoC container of database application component providers.""" - - main = providers.Callable(example.main.main, - movie_lister=movies.MoviesModule.lister) - - init_db = providers.Callable(example.db.init_sqlite, - movies_data=fixtures.MOVIES_SAMPLE_DATA, - database=ResourcesModule.database) - - -if __name__ == '__main__': - DbApplication.init_db() - DbApplication.main() diff --git a/examples/miniapps/movie_lister/app_db_csv.py b/examples/miniapps/movie_lister/app_db_csv.py deleted file mode 100644 index 9822ff1d..00000000 --- a/examples/miniapps/movie_lister/app_db_csv.py +++ /dev/null @@ -1,80 +0,0 @@ -"""A naive example of dependency injection on Python. - -Example implementation of dependency injection in Python from Martin Fowler's -article about dependency injection and inversion of control: - -http://www.martinfowler.com/articles/injection.html - -This mini application uses ``movies`` library, that is configured to work with -sqlite movies database and csv file movies database. -""" - -import sqlite3 - -import movies -import movies.finders - -import example.db -import example.main - -import settings -import fixtures - -import dependency_injector.containers as containers -import dependency_injector.providers as providers - - -class ResourcesModule(containers.DeclarativeContainer): - """IoC container of application resource providers.""" - - database = providers.Singleton(sqlite3.connect, settings.MOVIES_DB_PATH) - - -@containers.copy(movies.MoviesModule) -class DbMoviesModule(movies.MoviesModule): - """IoC container for overriding movies module component providers.""" - - finder = providers.Factory(movies.finders.SqliteMovieFinder, - database=ResourcesModule.database, - **movies.MoviesModule.finder.kwargs) - - -@containers.copy(movies.MoviesModule) -class CsvMoviesModule(movies.MoviesModule): - """IoC container for overriding movies module component providers.""" - - finder = providers.Factory(movies.finders.CsvMovieFinder, - csv_file_path=settings.MOVIES_CSV_PATH, - delimiter=',', - **movies.MoviesModule.finder.kwargs) - - -class DbApplication(containers.DeclarativeContainer): - """IoC container of database application component providers.""" - - main = providers.Callable(example.main.main, - movie_lister=DbMoviesModule.lister) - - init_db = providers.Callable(example.db.init_sqlite, - movies_data=fixtures.MOVIES_SAMPLE_DATA, - database=ResourcesModule.database) - - -class CsvApplication(containers.DeclarativeContainer): - """IoC container of csv application component providers.""" - - main = providers.Callable(example.main.main, - movie_lister=CsvMoviesModule.lister) - - init_db = providers.Callable(example.db.init_csv, - movies_data=fixtures.MOVIES_SAMPLE_DATA, - csv_file_path=settings.MOVIES_CSV_PATH, - delimiter=',') - - -if __name__ == '__main__': - DbApplication.init_db() - DbApplication.main() - - CsvApplication.init_db() - CsvApplication.main() diff --git a/examples/miniapps/movie_lister/data/.gitignore b/examples/miniapps/movie_lister/data/.gitignore deleted file mode 100644 index bc208b8f..00000000 --- a/examples/miniapps/movie_lister/data/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# Ignore everything in this directory -* - -# Except this file: -!.gitignore diff --git a/examples/miniapps/movie_lister/data/movies.csv b/examples/miniapps/movie_lister/data/movies.csv deleted file mode 100644 index 6cb91ddf..00000000 --- a/examples/miniapps/movie_lister/data/movies.csv +++ /dev/null @@ -1,3 +0,0 @@ -The Hunger Games: Mockingjay - Part 2,2015,Francis Lawrence -The 33,2015,Patricia Riggen -Star Wars: Episode VII - The Force Awakens,2015,JJ Abrams diff --git a/examples/miniapps/movie_lister/example/__init__.py b/examples/miniapps/movie_lister/example/__init__.py deleted file mode 100644 index bfa99aa2..00000000 --- a/examples/miniapps/movie_lister/example/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Example top-level package.""" diff --git a/examples/miniapps/movie_lister/example/db.py b/examples/miniapps/movie_lister/example/db.py deleted file mode 100644 index d519fb03..00000000 --- a/examples/miniapps/movie_lister/example/db.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Example database module.""" - -import csv - - -def init_sqlite(movies_data, database): - """Initialize sqlite3 movies database. - - :param movies_data: Data about movies - :type movies_data: tuple[tuple] - - :param database: Connection to sqlite database with movies data - :type database: sqlite3.Connection - """ - with database: - database.execute('CREATE TABLE IF NOT EXISTS movies ' - '(name text, year int, director text)') - database.execute('DELETE FROM movies') - database.executemany('INSERT INTO movies VALUES (?,?,?)', movies_data) - - -def init_csv(movies_data, csv_file_path, delimiter): - """Initialize csv movies database. - - :param movies_data: Data about movies - :type movies_data: tuple[tuple] - - :param csv_file_path: Path to csv file with movies data - :type csv_file_path: str - - :param delimiter: Csv file's delimiter - :type delimiter: str - """ - with open(csv_file_path, 'w') as csv_file: - csv.writer(csv_file, delimiter=delimiter).writerows(movies_data) diff --git a/examples/miniapps/movie_lister/example/main.py b/examples/miniapps/movie_lister/example/main.py deleted file mode 100644 index c029226b..00000000 --- a/examples/miniapps/movie_lister/example/main.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Example main module.""" - - -def main(movie_lister): - """Run application. - - This program prints info about all movies that were directed by different - persons and then prints all movies that were released in 2015. - - :param movie_lister: Movie lister instance - :type movie_lister: movies.listers.MovieLister - """ - print(movie_lister.movies_directed_by('Francis Lawrence')) - print(movie_lister.movies_directed_by('Patricia Riggen')) - print(movie_lister.movies_directed_by('JJ Abrams')) - - print(movie_lister.movies_released_in(2015)) diff --git a/examples/miniapps/movie_lister/fixtures.py b/examples/miniapps/movie_lister/fixtures.py deleted file mode 100644 index 0e8805be..00000000 --- a/examples/miniapps/movie_lister/fixtures.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Fixtures module.""" - - -MOVIES_SAMPLE_DATA = ( - ('The Hunger Games: Mockingjay - Part 2', 2015, 'Francis Lawrence'), - ('The 33', 2015, 'Patricia Riggen'), - ('Star Wars: Episode VII - The Force Awakens', 2015, 'JJ Abrams'), -) diff --git a/examples/miniapps/movie_lister/movies/__init__.py b/examples/miniapps/movie_lister/movies/__init__.py deleted file mode 100644 index 73331b9a..00000000 --- a/examples/miniapps/movie_lister/movies/__init__.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Movies package. - -Top-level package of movies library. This package contains IoC container of -movies module component providers - ``MoviesModule``. It is recommended to use -movies library functionality by fetching required instances from -``MoviesModule`` providers. - -``MoviesModule.finder`` is a factory that provides abstract component -``finders.MovieFinder``. This provider should be overridden by provider of -concrete finder implementation in terms of library configuration. - -Each of ``MoviesModule`` providers could be overridden. -""" - -import movies.finders -import movies.listers -import movies.models - -import dependency_injector.containers as containers -import dependency_injector.providers as providers - - -class MoviesModule(containers.DeclarativeContainer): - """IoC container of movies module component providers.""" - - movie = providers.Factory(movies.models.Movie) - - finder = providers.AbstractFactory(movies.finders.MovieFinder, - movie_model=movie.provider) - - lister = providers.Factory(movies.listers.MovieLister, - movie_finder=finder) diff --git a/examples/miniapps/movie_lister/movies/finders.py b/examples/miniapps/movie_lister/movies/finders.py deleted file mode 100644 index 557dcbdb..00000000 --- a/examples/miniapps/movie_lister/movies/finders.py +++ /dev/null @@ -1,87 +0,0 @@ -"""Movie finders module. - -This module contains all finder implementations. -""" - -import csv - - -class MovieFinder: - """Movie finder component. - - Movie finder component is responsible for fetching movies data from - various storage. - """ - - def __init__(self, movie_model): - """Initialize instance. - - :param movie_model: Movie model's factory - :type movie_model: movies.models.Movie - """ - self._movie_model = movie_model - - def find_all(self): - """Return all found movies. - - :rtype: list[movies.models.Movie] - :return: List of movie instances. - """ - raise NotImplementedError() - - -class CsvMovieFinder(MovieFinder): - """Movie finder that fetches movies data from csv file.""" - - def __init__(self, movie_model, csv_file_path, delimiter): - """Initialize instance. - - :param movie_model: Movie model's factory - :type movie_model: movies.models.Movie - - :param csv_file_path: Path to csv file with movies data - :type csv_file_path: str - - :param delimiter: Csv file's delimiter - :type delimiter: str - """ - self._csv_file_path = csv_file_path - self._delimiter = delimiter - super().__init__(movie_model) - - def find_all(self): - """Return all found movies. - - :rtype: list[movies.models.Movie] - :return: List of movie instances. - """ - with open(self._csv_file_path) as csv_file: - csv_reader = csv.reader(csv_file, delimiter=self._delimiter) - return [self._movie_model(*row) for row in csv_reader] - - -class SqliteMovieFinder(MovieFinder): - """Movie finder that fetches movies data from sqlite database.""" - - def __init__(self, movie_model, database): - """Initialize instance. - - :param movie_model: Movie model's factory - :type movie_model: (object) -> movies.models.Movie - - :param database: Connection to sqlite database with movies data - :type database: sqlite3.Connection - """ - self._database = database - super().__init__(movie_model) - - def find_all(self): - """Return all found movies. - - :rtype: list[movies.models.Movie] - :return: List of movie instances. - """ - with self._database: - rows = self._database.execute('SELECT name, year, director ' - 'FROM movies') - return [self._movie_model(*row) for row in rows] diff --git a/examples/miniapps/movie_lister/movies/listers.py b/examples/miniapps/movie_lister/movies/listers.py deleted file mode 100644 index f00b22ba..00000000 --- a/examples/miniapps/movie_lister/movies/listers.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Movie listers module. - -This module contains all lister implementations. -""" - - -class MovieLister: - """Movie lister component. - - Movie lister component provides several methods for filtering movies by - specific criteria. - """ - - def __init__(self, movie_finder): - """Initialize instance. - - :param movie_finder: Movie finder instance - :type movie_finder: movies.finders.MovieFinder - """ - self._movie_finder = movie_finder - - def movies_directed_by(self, director): - """Return list of movies that were directed by certain person. - - :param director: Director's name - :type director: str - - :rtype: list[movies.models.Movie] - :return: List of movie instances. - """ - return [movie for movie in self._movie_finder.find_all() - if movie.director == director] - - def movies_released_in(self, year): - """Return list of movies that were released in certain year. - - :param year: Release year - :type year: int - - :rtype: list[movies.models.Movie] - :return: List of movie instances. - """ - return [movie for movie in self._movie_finder.find_all() - if movie.year == year] diff --git a/examples/miniapps/movie_lister/movies/models.py b/examples/miniapps/movie_lister/movies/models.py deleted file mode 100644 index c42d6903..00000000 --- a/examples/miniapps/movie_lister/movies/models.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Movie models module. - -This module contains all model implementations. -""" - - -class Movie: - """Base movie model.""" - - def __init__(self, name, year, director): - """Initialize instance. - - :param name: Movie's name - :type name: str - - :param year: Year, when movie was released - :type year: int - - :param director: Name of person, that directed the movie - :type director: str - """ - self.name = str(name) - self.year = int(year) - self.director = str(director) - - def __repr__(self): - """Return string representation of movie instance. - - :rtype: str - :return: Movie's string representation. - """ - return '{0}(name={1}, year={2}, director={3})'.format( - self.__class__.__name__, - repr(self.name), - repr(self.year), - repr(self.director)) diff --git a/examples/miniapps/movie_lister/settings.py b/examples/miniapps/movie_lister/settings.py deleted file mode 100644 index 5aab4184..00000000 --- a/examples/miniapps/movie_lister/settings.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Settings module. - -This module contains application's settings and constants. -""" - -import os - - -DATA_DIR = os.path.abspath(os.path.dirname(__file__) + '/data') -MOVIES_CSV_PATH = DATA_DIR + '/movies.csv' -MOVIES_DB_PATH = DATA_DIR + '/movies.db' From 2f42de9fa14786c9b99042bbaa22a82a65f92565 Mon Sep 17 00:00:00 2001 From: Roman Mogylatov Date: Wed, 12 Aug 2020 17:06:40 -0400 Subject: [PATCH 2/2] Bump version to 3.30.0 --- docs/main/changelog.rst | 7 +++++++ src/dependency_injector/__init__.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/main/changelog.rst b/docs/main/changelog.rst index 01cde170..470c7ace 100644 --- a/docs/main/changelog.rst +++ b/docs/main/changelog.rst @@ -7,6 +7,13 @@ that were made in every particular version. From version 0.7.6 *Dependency Injector* framework strictly follows `Semantic versioning`_ +3.30.0 +------ +- Rework ``Movie Lister`` example. +- Add tutorial for building ``Movie Lister``. +- Make some rewording for the other tutorials. +- Fix a couple of typos. + 3.29.0 ------ - Update README with the more direct message on what is ``Dependency Injector`` and how is it diff --git a/src/dependency_injector/__init__.py b/src/dependency_injector/__init__.py index be2daa3e..15726916 100644 --- a/src/dependency_injector/__init__.py +++ b/src/dependency_injector/__init__.py @@ -1,6 +1,6 @@ """Dependency injector top-level package.""" -__version__ = '3.29.0' +__version__ = '3.30.0' """Version number that follows semantic versioning. :type: str