From dfb5b4645be717c7dfa0f3111f209d9142bcdd3a Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Sat, 24 Oct 2026 23:58:22 +0800 Subject: [PATCH] Add README, three-language wiki and screenshots Task: MIL-005#1 Task: MIL-005#2 Task: MIL-005#3 Task: MIL-005#4 --- README.md | 156 +++++++++++++++++++++++++++++++- docs/images/sample-run.png | Bin 0 -> 54218 bytes docs/images/tests-run.png | Bin 0 -> 7378 bytes docs/wiki/Getting-Started-da.md | 63 +++++++++++++ docs/wiki/Getting-Started-ms.md | 63 +++++++++++++ docs/wiki/Getting-Started.md | 63 +++++++++++++ docs/wiki/Home-da.md | 18 ++++ docs/wiki/Home-ms.md | 18 ++++ docs/wiki/Home.md | 18 ++++ docs/wiki/_Sidebar.md | 14 +++ 10 files changed, 412 insertions(+), 1 deletion(-) create mode 100644 docs/images/sample-run.png create mode 100644 docs/images/tests-run.png create mode 100644 docs/wiki/Getting-Started-da.md create mode 100644 docs/wiki/Getting-Started-ms.md create mode 100644 docs/wiki/Getting-Started.md create mode 100644 docs/wiki/Home-da.md create mode 100644 docs/wiki/Home-ms.md create mode 100644 docs/wiki/Home.md create mode 100644 docs/wiki/_Sidebar.md diff --git a/README.md b/README.md index 450d629..2edd34a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,156 @@ -# 040-personal-flight-club +# 🚀 Personal Flight Club +A flight club (Day 40 of Udemy's "100 Days of Code: The Complete Python Pro Bootcamp"): it reads destinations and price limits from a Google Sheet, finds the cheapest return flight (direct first, then with stopovers), and emails every customer in a second sheet when a price is below the limit. It is for course participants who want to compare solutions and for anyone who browses the repository for ideas. + +## 📚 Table of Contents + +- [Overview](#-overview) +- [Requirements](#-requirements) +- [Setup](#-setup) +- [Run](#-run) +- [Tests](#-tests) +- [License](#-license) +- [Links](#-links) +- [Appendix: Project documents](#-appendix-project-documents) + +## 🧭 Overview + +The program keeps the assignment's class and function names. It builds on the Day 39 project and adds three things: a search for flights with stopovers when no direct flight exists, a customer list from a second sheet, and a deal email to every customer. + +| Module (`src/flight_club/`) | What it does | +| --- | --- | +| `data_manager.py` | `DataManager`: reads the `prices` and `users` sheets from Sheety (`get_destination_data()`, `get_customer_emails()`) and writes a new lowest price back; reads all its settings in `__init__()` | +| `flight_search.py` | `FlightSearch.check_flights(..., is_direct=True)`: asks SerpAPI Google Flights, or the course's mock flight service, for nonstop flights, or for flights with up to two stops | +| `flight_data.py` | `FlightData` (with `stops`) and `find_cheapest_flight()`: picks the cheapest flight, with `N/A` when there is none | +| `notification_manager.py` | `NotificationManager`: builds the direct or stopover message, sends it by email with `send_emails()` and by WhatsApp | +| `main.py` | Wires everything together; `python -m flight_club` runs it | +| `constants.py` | All constants (URLs, origin airport, currency, SMTP port, variable names) | +| `cache.py` | Local response cache that never stores the Sheety answers | + +![A sample run with mock data](docs/images/sample-run.png) + +### Glossary + +| Term | Meaning | +| --- | --- | +| API | Application programming interface: how one program asks another for data | +| CI | Continuous integration: the checks that run on every pull request | +| GBP | Pound sterling; the currency code used in all messages instead of the pound sign | +| IATA code | The three-letter airport code of the International Air Transport Association, such as `LHR` | +| SMTP | Simple Mail Transfer Protocol: how the program hands an email to your mail provider | +| Stopover | An intermediate stop of a flight; a direct flight has none | + +## 📋 Requirements + +- Python 3.13 or newer. +- Runtime packages (installed by pip): `requests`, `requests-cache`, `python-dotenv`. Email uses the standard library (`smtplib`). +- Optional, only to send WhatsApp messages: `pywhatkit`, a browser and a logged-in WhatsApp Web session. +- A Sheety project for your copy of the Google Sheet with a `prices` sheet and a `users` sheet (Bearer authentication, PUT enabled on `prices`), and either a SerpAPI key or the course's mock flight service credentials. +- An email account with an app password (not your normal password) to send the deal emails. + +## 🛠️ Setup + +Create and use a local virtual environment, then install the project. + +PowerShell (Windows): + +```powershell +py -3.13 -m venv .venv +.\.venv\Scripts\Activate.ps1 +python -m pip install --upgrade pip +python -m pip install -e ".[dev]" +``` + +Bash (Linux, macOS): + +```bash +python3.13 -m venv .venv +source .venv/bin/activate +python -m pip install --upgrade pip +python -m pip install -e ".[dev]" +``` + +For WhatsApp delivery add the optional extra: `python -m pip install -e ".[whatsapp]"`. + +Create your own `.env` from the template and fill in your values. `.env` is ignored by git; never commit it. + +```bash +cp .env.example .env +``` + +| Variable | Meaning | +| --- | --- | +| `SHEETY_PRICES_ENDPOINT` | Endpoint of the `prices` sheet in your Sheety project (the Day 39 name `SHEETY_ENDPOINT` still works) | +| `SHEETY_USERS_ENDPOINT` | Endpoint of the `users` sheet that your Google Form fills (columns `firstName`, `lastName`, `email`) | +| `SHEETY_TOKEN` | Bearer token of the Sheety project | +| `FLIGHT_PROVIDER` | `serpapi` or `mock` | +| `SERPAPI_API_KEY` | Your SerpAPI key (provider `serpapi`) | +| `MOCK_FLIGHTS_APP_ID`, `MOCK_FLIGHTS_APP_KEY` | Credentials of the mock flight service (provider `mock`) | +| `MOCK_FLIGHTS_URL` | Optional: override the mock service URL | +| `SMTP_ADDRESS` | Your mail provider's SMTP server, for example `smtp.gmail.com` (port 587, STARTTLS) | +| `EMAIL_ADDRESS`, `EMAIL_PASSWORD` | The sender address and its app password | +| `WHATSAPP_NUMBER` | Your own number with country code, for example `+441234567890` | + +Keep 3 to 5 destinations in the sheet to stay inside the free tiers (SerpAPI 100 searches a month, Sheety 200 requests a month). Flight answers are cached for one hour in `flight_cache.sqlite`; delete the file to force fresh searches. Try the program with a test `users` sheet that holds only your own address before you use the real customer list. + +## ▶️ Run + +```bash +python -m flight_club +``` + +or, after the editable install, `flight-club`. For every sheet row the program prints "Getting direct flights for ...". When there is no direct flight it prints "No direct flight to ... Looking for indirect flights..." and searches again with up to two stops. When the cheapest price is lower than the sheet's lowest price, it updates the sheet, sends a WhatsApp message and emails every customer in the `users` sheet. The message says "Flight has 1 stopover." for a flight with stops. Without the SMTP settings, `WHATSAPP_NUMBER` or `pywhatkit` it prints the message instead. + +## 🧪 Tests + +The tests use no network, send no email or WhatsApp message and never read your `.env`. + +```bash +python -m pytest +``` + +![Test run](docs/images/tests-run.png) + +Optional checks used by the CI: `python -m ruff check .`, `python -m ruff format --check .` and `python -m mypy --strict src tests`. Build the API documentation with `doxygen Doxyfile` (output in `docs/doxygen/`). + +## 📄 License + +See [LICENSE][license]. + +## 🔗 Links + +- [Repository][repository] +- [Documentation][documentation] +- [Issue tracker][issues] + +## 📎 Appendix: Project documents + +The documents a newcomer needs, in reading order. + +| Document | What it tells you | +| --- | --- | +| [AGENTS.md][agents] | The rules AI agents follow in this repository | +| [Business Case][BC-001] | Why the project exists, its scope and success criteria | +| [Stakeholder Analysis][SA-001] | Who is involved; the stakeholder IDs used for owners and reviewers | +| [Project Plan][PP-001] | The gateways and their schedule | +| [Milestones][milestones] | One document per gateway: Go/No-Go criteria and tasks | +| [Architecture decision][ADR-0001] | Why the code is one flat package that prints to the console | +| [Artifact registry][registry] | Where each artifact type lives, the next version, the PO language and domain | +| [Framework README][framework-readme] | The framework this project uses | +| [Framework changelog][framework-changelog] | What changed in each framework release | + +--- + +[license]: ./LICENSE +[repository]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/040-personal-flight-club +[documentation]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/040-personal-flight-club/wiki +[issues]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/040-personal-flight-club/issues +[agents]: ./AGENTS.md +[BC-001]: ./docs/business-case.md +[SA-001]: ./docs/stakeholder-analysis.md +[PP-001]: ./docs/project-plan.md +[milestones]: ./docs/milestones/ +[ADR-0001]: ./docs/adr/adr-0001-flat-module-layout-and-console-output.md +[registry]: ./docs/artifact-registry.md +[framework-readme]: ./framework/README.md +[framework-changelog]: ./framework/CHANGELOG.md diff --git a/docs/images/sample-run.png b/docs/images/sample-run.png new file mode 100644 index 0000000000000000000000000000000000000000..553a7768114b783fe09eb94689652c93dd951059 GIT binary patch literal 54218 zcmeFYbyQqmw=J0bLI@#Pkl>Qw5FltF0YY$hw*+?x?j$4x3oeDbTj2yNVz?AekOG3c zyVcv|yZ7tYukRhB$9UcSS7-1?rN*gq&R%=3wdb01?obsaX>3d~%sY4PV9Ux#sNK1P z#((F|-2;q&z&9TR*30kQ8HtgV5YzBX+d_I65S*eO?8#%$eW(;mqzU6PMTWdN4J?9Ie|=_mA2)f2uiVxX&Ui_s9(o zE6~KOl6B(3Grl{{L<+;+N!O>G{6a!HFAYszOjaAmX~@y_mUA#7C~8eOCIutO~0o(jtYCb{eoOT2wu71Y7p|e%2lg}!mI`lgbmY( z@%ujq#`X+SN4-eeI<#d#6Kr_o>HxhF45JCz^5E!dpO{dV{hfZYUHeZves|f;q2DoE zb3y_k-dwYHgMZNbxu(m(4604?r|KYfd%|yI0zwigFWpJFQmTfxjwNx+?h1l};WtgAawsf$WaGVL9Y*n}va!aj0KylB%6(0V0b-6w{ zm#cIh7x0L+N7GgR_~=X))=CM19KMtJma4z{D|pz!cBI`aJ5rZ8CC%WZd5tmPCh_1Z zgI_zPe0mPqR)2X!t(WA!v|=uNe%SDpJU~uYRgYmj7m_K^*5Nf@J@<)-2ntcy=CYP8 zt+PXt%S*}J zfjQKf4?TS?h#L!Ds}z)2&e&hzUT7ij=g$xm<3>-6X%B+Cq`f?R8|`SQZ~TK-x3H<2 zgr^a@`dNEIc~+=0qo*zh{K|ac*}YAp13P*Lf2}$V)8_9_#0~Un>mEN05}-B`7O7#~ zHm7SJM-9U@h54J@Mpw93?lHE0b;=qq14;a<=Gis@5m|b{%#V=#vWhZOo%m#f31os> z#ODfOeAN6#sBUWps&#I_Jm_tqKbn}?FOdOd|LKC&kN|F$PAg>p{?Cu4=91?e>}av~ zNLbLWh>3Bc10D!AlzIm|2N`y;_y+<(P@PtW2RPO3Qc!DNkDJA6k(WLuS9)SWmyt5P z+xO#p&^k561clvXc9f?vQL9#D>AhSIYJr`%r#CmtA~g*~q)9Tms%A+;BZq~(Y&b(Gk0ycHI)KT}E_P969K^78QJ`KAVv;Cu-I` zcopwqPJchRa57po>{CkKFlMY<#yrwz1+lwp_>#9_E+1Q6KK!6|S_?O4qmT<>8$~CA zdFg4mO<=5TEVj8LK4S7r&Y62-^rFrvj&QTbcQDT=gFJ_Wx{>&dAc09zveRlb5id~c zax?gyw54lffR*}~Mtw=*je2T_*{m(NP=SWhnkkl#j#Cl#?SDYk&PxHb=l*aY(*H#BMiaj{cit_s#e(f zH(9gc&49o$tSKup{pfHup_8{PrL9f=u?sPz3R-91v3GY23FDn}IVf|6mXDq{$H)p> zTqQvXiT&S*>&lABDov$}`ad5~SbzEUgQxL*$>r2$ZMv5&>`coY1|qL>hmNPFA7ejV z-|r8s#hqzUpvO+{v~(x3EaW8AQ835u_MsPP5}Um*!e1Xj6OxrI+}$qDo;YmToj2h` zX2`yjM#b0Qfxz#tX3>*T|`>D&hIdAczBtaiMvF!L}<<+D%1eE)?A*s zGpw$n7Wxw2+Ob)p`mPpx6l)5Eztnb99?rl#o&ZMMDhv_1^fCSw(@)4fV?Mlq=|9>3 z-dvJY7Gz`}1W{6OfLRT@@XT+2$+QYhT*{M9KQetWNvNNB0S}FG05M`>Dpu*^22tiA z7)EE2jdIK|6nS>OP8pdfHa$yAIYBXMhs?Ox@pTxw+>>8uPdg*2OSJE9o0}0~d`?`u ze!QL?CJT!>XWkt#7S8iDy9=XR-&Z`?tzPKqh7yj|J`8+QY_*o;ChO^G*pe@!69Y<* zyFR&~xKWE^PffGtsd=e22(D0Bw7ZUvG!m|L50qe}}>GgbW?gmDcbA=l-F?B?7^P&s--K?RSlElp>*j1ps z<-TR9jsI06q`~GNh-jP?G|rMa7P4SG1r56^A7G2L5Aobz9$WnLfDeyrYdgJdT1aVo z=R$n6o6^p0JJNL3tj#AT^Ga($3H!xRRnF%f?NpLi-&mg*Ye$6Tt9j8HX{8w#w%Yt1 zVono|SZ=~nfx0M1`cqg|-s{hJytzYVpOqkW9t&3FOiV25qR#0j%S1~W{shdG8}7ch zL!9M(m32_%ftOlb_Qd7f2_yo+y&r-?{V?OKA6g>}yLvKJD71!kffmW;NDBLVWa&n3 zJXfgF!zCp!^PT!XQCHQyT4N=6@%xSKpGI%?QJi@)#S6(zJEhnK%}^YY5wI*N#vxO}6%_(xrKL|*n;P$vkr@tDx}cUUJtTi%=A2iN@zJh# zmp`&)q{^`IGtoj!x53Ibh5dx}vYp|dqF^OF_2e28l%lhvD?Q+?4OjF$JmM>>Uh2zE zwAc>EDYs6L5uN`~KUf@mKS7t@gdgg#Ym|KXC;#Vypcq2>2^8_a=c zYetmg+uiCR-t4LXPd3YH9R2%X= zITa}>wCrlInuy8%rp9fxSZQUcEBmLW?dA-kOBdGq*u-XcwAwUt0wr{PzSFJjKpmDUj3!Kq+M-j4dR#@nBzPuE1IK=AOFObtYLzu{ta^}chZ(O zMYex=^wF8tMQL!odScxsB(k`yV`z96)X9nA=R|QsQib~BYtQCl){@qy z!hvoDxpx_W&dYkH-hOk5l8R_BhKH5xLoKjH93*M{R=A0MlGoWja^c`p{Svu&fW z^nEnw@Ozjmt$6gtr&n>6&$LqS$OT^11(upNuk6*>Ok@lc~;au*&NPn7nBe)UJ*5`8d=T7^0VKs*sUP{N4 zjnCUZa2@iDv_Yf?6$Zb2A$!A@2X|@UOe%YULLHG8Z`xkk2*3ZXhVm|H5{7dpL)Sb`s7>(%RzWhq8mLf%TcY&~M6mvq~H*k&Z}@4ZSLuv;+D z3wiUqDc1&_;rk=>XejmVyx#M;RvY4EQBbBK`*181bx=JF&F&;$oX^V3ht1?H2M15I zH-xram2I64DbQ;&NMj$XgPpppNd2bV2g}}E2#<&VZu)U(yTP$BBl>G9T=t(?=u)<|b^62s4(_2pvJ_|wzZ$47f>VIjv3{$m8c(vA=shqZ(g z`xt-SV|;45eC_U+t4#@_{0;gdq@dnyJbO`YC$G&UN62mcWY#EL(@?X9#=w7kBozAT8e~1{=-yW^0 z;^16=`GVEkb5>RLPE3r^xFsa}Yje8cM11_{MgGdliekpg^k`1S1lDdhdfh@LN*-R` z@$zY%sOFOnRu~VxP0PVJqS`z>7@Pbx-X|Hlp8o#ua32DE=bi4jLToqR$*bqLON2)^ z>&G9btE>8wl9H{-8q}Yk(yy~CO{G&+bEQ;pHC{bgTU*0Ji%CwF<-t`{n;jk=e*3dy zaNX6(-!d_4OGy3Sk?#E&17K7PzR`$}8e zazz1Gf0`W&omTK-c?anu>MKKo@ndq*`ovDzncQ-9e_&v-yDf@Z)N614ad+|3?@(bM zcg_x|Y|ujN45$#%bsvvU-s&|@-h#g-2d5p}`ArJh0j#yfB!-{((JmPS5%agnySw$>s(P*&gQXmlq&2ahR8rtQa&gxVrug~_U?={ zTnI)tE@9=A?)+}r8EG@t!^FXXI@xai@F5uOAF*;b$@}R>0(!D^_^LA*a$dIOs|N>| z_wM;Q?s`!Q>Z~p;Q3&9>Zcm|XTKT%#A|mv?Jiku<*5M{`<}ii}x~~21g~Ezf4PJZo z_xI1t6zJ;e_H=i*x3^OYdoK*-b`t7GV`i%)rKK$_)*z>*hWqVP_*K@C&8urev~17& zKE3`k-LM-)A#k!a`G{q;ypAZZj1|QCJ%boGmyP~zIxo*CB2C~izOw@&!;jY?tE^AJ z)5+M*Ze}38ZD{>yvZl4kV|Qgogqw}cu%&;jlz1RrsLf;dhAfIkK=Y;h!r`m@02KV6 z5Azeo_o#%ux!g9EWaAjVHY4O@Wauz*+-w(HeeP8lwNjCI>KGc@Utdu3^6;dk8}{_{ z*sxW@;ja$rxg5sv^Cj}a4bAp=H;~~#;aT$&L0#Hj;x%t)1D}fjQprwez6Z$eO-N)S#Khn zyVII>U6}jU@7a=sKWF=K4+B-SwT-Jxy4Oc6jat3Sb8@O(*I$#9+j^Ypnjv_?R?Dl1 zHVkh!%sX>?WyXn#+txgK1~aJ~_AktP=$?eHFD|AgB&5g0H2Pge9you`lO2#)Ty%=B zcen;Q;6xSWak4eXL)PEXk)lxDuq$0S;;}ocucMO`9qqaBi`m>Oh9l< zSDX9j>FHTpua|1WHdCq?l{4&TzS6U?33GFEKU??^6eN()#Kpzs`Yze^!nJFCyu84h`du7(>5_`FvI@P}dlBAVqD6iz++5lTr4b3!1dFG%X@X(X}Gb2V9WsRIn!O{z|IMRgYW%%HD{5_%BO_*MJ7 zrdz5wdYzNyrO~otVOSVuqs)M5v-`GQPji*fk2fCYbNcu;$zXn+c6(KBEk&v z@;V3QHCfol-`N>6St56+BZ7Q;jbBGwTUu5&)QUes*U(xyS|90cH?Pi|t0(2QkQ0D% zp7V$MDlrvQRCKH!wktQ~sV>TR497DQAd!Y1Tfe;ySJGo+@v*Uwqod;_?pc5N8*D2( zHKb){XMpG7%63=G&t!!lGKeQiWcJ4ogM-p)7nj2lB$=9WIW=QpRZNWPqbJWv1zkr! zJyfrEJk7m{rsL!+-e7tJX+ZaJ30i6ji>$bM*y6EEg&#Rc;x?vntWYH(DcKI?Fg`v$ zwtPWSP_UiX=cJ|OZedZ7Zm7LaCYV0k+H9yXYMQMg!&9(>+{VYp=P<-9=hU&m!`~>e z*N+=1jM|;OwjJx9B%`~9XIZm(el(SSmpBaS-zO$IhpSry0|RrOrT+XAqOP2Qwc^CT zIf43JptxDmdQ}?s#rJoiG7MDg@vk42Mlb$2dx4-&)$^)qYQE)4cAGl=5!4Hc&I7DU zZLO8QbfJ-NuQCy<5Sa?P2~7!!rNdQ@mX?`dEYiH3oZ%eFM<7KP*6)(RDpE=^=%H4F zS$7Xt`fVl#K6RVf*x2Au3VyAvro4^ zQ6{e~JiK?Q&Q{yn#^&Js>esJ4m(?2v^|=NXAJ5eq;gyRDH{S%Vl3)_qw>@h7e`L}P(?`1w0K>{eo`7>{^CBW=X?(L)STU2l)uX#br=u(+G!CWg-G@ zUV8uek&k_F(lI!=X>NWc8<&=z*0FznRj8cl<2~@Wjkak2YQNK;FLimdIq3Tvu(2JE zD5rwTpS%E%)&C%8h-`AR+?)R(ARw^xvDqe1_7=ulrJNS|hA%BGMN$c?7#J*Uw|*@v zJ9r!N`SV3b#3mLtc4tJYj;iX=DCw0_R>m!4d_{)M_vA`{2u!cAFw*DrM>kUt1;%6I zQz_l#+_JKn(Z);v|GcuFU8TFLtE;znbFK}RoLmf+URYSq`@1J#Bixrnsa86K$E9xftRwgN1XWPJ_qeHV$Vkvb?>zs9}ZV zKNPbsw(f>)x8L1)b@IR6rBqWDSy>fj^1oa9_M!FplhWeDf2ncCp=A$P(8LCZ&aJ zLQPIiOz3*h9-d^g%*MnN5`}VFkl4$Wj>?B&+uHV~3tg8~Ftf2y^4TfW^^cB?MNv#5 z{y2fE?|QIg9Y$}1occZDA{C4??kXtEu(AyIdobJLCH>C$cIfY6QBSX*c3oNvrdPmV zt1EoS36I_;H}ao9X+siqoL{JiI%AIk`3&Zqnk1wzRA$B+WaYY@KI|KhT~oP%p&!{Fx1)?f!m6 z<`rXcaliRjm-5ip++1A6cCfrW2|P4tZ>}1-n#l{2@F)FkZktMGW*X@y8$-F%=|ZQU zUO#QL_;%;P{rf=<$SpFuzuV{?uBqcv765^0X@7qG+OT8-FviB1ZFgHub@jb_xVzI0 z&7jW$wPV3B%hds@oMwZBI*bl3}InI017^Rk_e;$C1(YZY3ktMy}z)`X)5)OKW$-Q zL68#rb~VkDKhhw<`uyPoyt$b*ynU(-^(JC^X=Mc;EjuSi&&}NP%x{10?p>dM{y9+* zFa#YKKw#&Gt5(+5VLuaWoSmHul~0S*nMX%i4inSU6>od$*Z?!jRequ7=ztoX=lkC5 z(9K=yZ_Cb`s^nv!QZCNV%izRva@#h%`EKBId6SV*Q&Y{tom*JAxxgjxVo$~0J@WHs z!DD=878ZQ8?~D5y8XDs;RInpc zru^6Ju`xfV>PpMY%YRLW1ov*Pt?>&)=+;=|XJqVv8{3v+hlPcQQ-i*4x2<<>&g0vi zOZCEmXj;qI*w}&e%jODt7Pj)|ssL3}Q8{Q=7|=3es%vV-44VEc$d}2-r5$<9>I_ZI z%{#xVz}74TW*SfyPoHKwtmQ-F<69h%QlKZu6lpauV7NTDPvtbV7=x=F?}2{$p#Ekc z-6ytz5HrZy+8RuTpvMl6>l)s>tY_JL8+6V}ot@!2&bCkrIP=F9zj{Y3m_D$282WiUU!F0LCPOqp3{L`d+}1o( zj&0X+10ST&eT#j=hGD}HeGN3Bvb<1c}eUedS5D})KG|kA+ET%Sip5{9nSZF=0{a z`2qtO+oWXP+n_H*6SJFMkyd&yb>rfkqJa17<~kVkP^hcJG*4;Juue_sJ$bUCrG;|W zpv732|DH?DXpm7@Qj$|#y#EuIm4$^RFpz9(=*jKdzb`{vX!cVFY+2Um0_KDSt0SD4 zQ}nvldUHIJc}K`?!_31&d;gh$|1|)0H#f}~fnPm1X1cnZ#!8kiF1&q3Q0IqhL64vo zfRz&1B%Zvq9WOg)XOEkxFw8G5?p{44AV6{Rf1(IdQ&o+Liz6pKwdjr|LS$%)oJJ3y z?VGOlr%K|qg+@eNp0AdJ!t5saqg>wu9hWL8E{=fmIZ(aP0c~rsu|AuhPE#}@+R2$WB#i3xG z#|#ukV0tbaAC9Kj^LP(z7no+37ZuCkNpzL9sb%IjR1RjiTD@%i@Ln6nzuvJ+N zThFTZc|b)6cL)uENvf(39S$Eg-#w*%AWRE4Rx_%4^uuzQk%%JIv*exg1296BduS}j z61k6!snwGOEG9x)aJ{}~tYVIY{o3R0Q7E=KXGq(5^!djTRH5G9OO#2J zRi9;`Q{2?XVEMHC<8$9_c7M~x&{&z-^9>NX*-%E|^`k?AeXJA)Cqx~4*)Qbu!k}88 z#}_l;`v)^6!EgPYzfW+es>c+x7<0`B-z4VtJq*NqC^=(bw6a;oSeU^mozFA6pKo`K z=`R_Sm6OeNJ0Y*`XB7F$YaGRc{W}G~`t`G1WxHNEQh_6$1K z^;)Yd&U3RNxTxz7XA@Qr>2m_5U|Von{Ksx;wi(!h>&Hpvh2|ysSxg*8q)Yf|hZS6o zM9Z)15=*WVj~ym;+xScA4qlCx_P#?Cb9G-I^bx~#UK~&tbj=w4aCQ0TPv_~Wl$>!6 zhoL}bR9cLBbgIYnrr4X&kKdTMG^Z6YXas>(EWB#Z3$RVyPXy5n`IJ)z5x_l> z&VMOuAI+*&6Co8f>5iU$Bw`dtihCle;1i>&KEd%)6(IQcyG$z9Z!5NsI@glgjDsq! zGXrycA&83H07`%V?&VJ*c~^O_53}XaB4|ox@~X!sT?GWaKDKLEDE$`YThv_HXD+mM z^`_pXBEXFh5;=E$yV2(N>9Zqc8dco+YFeA*>!GtbX`!pZ-L08J4X%PIHS-xq+FjKC z-a*P{B5nWW^R47-KFA+`lXB?_Hy#=AbM?&AkU%h~~xPErlbb;i+x^!()8eug*mj z#{DX8wWvz+>0JXI@AiS#!}rt|`O=n4{#~VZ-uDthA9T`7F=V=n_VzE6P1RFWA!B+t zZP{S)UKMgWPy3YF2VZD05)<+MPRxByMdb~G(XD2Vql0uDjeIMnuB5%qq20!+B_{i} zuj29g5Gi9p1~cmlx$AHZ$y+1StiXSzhM}VJjH4_p&7%<#(5SDap%t%s6@@7OQTt3N z3i$2_>4>|!M-oJoj+Tx(lRns90Ms#5V$i2tf~8?+DAZj;GQszW$TE>IpV)BDRh$XW zJ$<}*V?T~M-$>V`^+jC~Q!D?CQDBAnqcBoa7H-FLWW>!TeOnT0Y3UF=W3y^IU$okr zl~=!EASzPS&-aR%BT73-kI-%DA!)wKbJd82U52dtH(VS$H;rZ)bzx~63lg6 zl%t7Kg~$_J4mPI#2t>Hx)u-aVQLZ68$8T@A94?R%%{N-#2wn;t2TB!^x{gB+^6C<0 zek6zo1;+lUi-61`!-)6Ll$E6e`f8|>M@!p(j!}epgrMTCb0$mNi{V0Oc6aw$ z*Q)jUrc1IryLhca4V3)T^o+EO(sc3RjA|MEMdO&q%_h9lR!r8+@P|Iy@v40se>&$B zF%BD#&n<|qFrRkZc8uRV2#PAletYvZ?K)gfcN@_W(n~UI>eWZbp^kok8VStJVuIP1 zSkIIaiP6T_CUlV$RKvpkQft?+{5dmcw7(H7F)Z2EkCl0coi?C^&)C|U+fGZfX|*EG z+ONK%S_wET+AVBrwV*d$_LK^1SQovIP$yg^VRE5xk^A&PQR%Q7)c5hu#%wecia-Cr zv10ybE84;C@DH^>ny$U-;tC2DX*{RKXdSFBH6!hUXCv@(|pi16knS z9y2`A-xER25{sCQ_uZ=K&@z<`XkTeTEUz3~u&s}I$vrTq9DwruB)RQs&MKPxnQbZ` z2F@?a)n=pKF*`3`LrbHh7uHf^R$2v$cnhtC zHd2`5X&YxX3lmYL2FhhxTQn`|!#MQw=YW2E)zb^Pw0_TCc)0iH-B68TwOY5$v~w0# z2)r7qp&sMAtbx(9;b$h`C8(Y(>u1n2(O2^_^$#UyYT7aD`r8BM3A}?L6MC|tp~P9{^ZQEXZzfgF=&wXquaGVd4H+)Y$3}y+$6nrNyN@&5It!e~ITVz!z>ww?v(&T< zyj(i8U@adA)JNZF@3HoTohLxd1Psh6BW>izK84UZ;54{Vo2mvC(KJTSRMVxV9?)(7 zsAiQ|R3FwTa}U=Qy^4*J)G~#dL47kWc;zK13xi@S4T{E;Miv)ptDpHVe9ZG{6UBSD z$@Qi!^5bQGi}MC&`g@(AS2{*|?8fE;cP=`IuH0x#lrwCP^BYM;+{w0POcRl!A`P>j zS{I2AyP+ER*mN9EQNul@3g#C2m*e#gh8;J2ELI2WHgReiI0e!Z9Jll1(B$t&5vpo8 z^HZheGsuuryM#TO&=%YQWp<4rZR;`Q3s5buTG8VC%LUltf3-ccZ0jGYvwMngIP&V& zDS2+liY%dB-;B6op32L77g20nF5xz89orwg_lw&o;Chy0?^{Uj1O!>Mc`rDYf?a=G z6tWiKx4d`Ybr5#o+NiAUDD;3};{igkYocTrS&<60H>}Y%^|I}+;rZf2y8LUkBP~Gk zguHBgy;LloD4{(q;JU3Jvb?m;)>l`OpONl!GxfO=U6dJ&W>?P$R!M{8qhCU6iYwC%Og36whcayy{*<;?SXH&b;Z|+U1^^B!6MG`ZXnL4JKsoiAYj z_vzo&`d%pL<#5yp^&W4s5TD{qNcMYjqQR|^4Tmiq$PLu+V5(ti9%<<5SoPK{vwcpD zm=rL$m=87+8Li#T$(Sn8!+IhvD_?w#!F?C@lI=pY#=mOpx5KId0~Lkz0R1fx+jHaT zgI$7qCu3VP8hyE(YaXPA5uojFsM$&CdO_FI1r_UynM!5+)5v&^6;v+@1vC%iFbVlF zsR!DgVdS*bzC2fzE2WZJ`kl~Yt!Y{#ZgD+^EZsa4&P}3V5a_zhWNR=Hap0i;{EsQv zW=R#$IWkY5zTCg)AfGXiGiolLN9RK-MwmhPwERc31GBH6wt*6xs1#|??z^cP>#1EaWMk< ztkUmEmpa4O1zV+;<>`zCx>cD0&T&r}&R$J(=#ElHrTO^J?5Gk(QCv^RODejf%v&D@ zdLAO059KSqW@hRYw)hpd#2!A6XukXS!0x=NS!$5fxfn6bUf#qv6+qJNN8fOBslUHb zK8lW7%CgB)8-U69r99!&W*G0jfE0lwVZL@4JbIZCZC7DgBAalT_gLM?BcN>+jJNU+Y41b6{6HS}#fSHQ?2> z!Kvqh;l+JsB`=1UZZ8|uz$@ah4YIxk!p(OX*q+xe6E1;tt9iOT74;&(gB31TAojO6 zDWaZC`G5)MicxWO-#K?MGL$~YHZ84UK_j}~9i%EvC5q$yx&HQ+?-@oZI{%Zl6#tw3 zK)?Cl^-rYeW>tGuod!J$;M=%OLO`Hg(rzki0A;kh-(F9B^`8`Uy<6W))?_S;%Kk}) z&q`k?Vj@$B_@(tQTF%qolWyi!I>*xu^3Vb8iZt9ukLoI4I7at<(`HcI{EPJuu{A&! zD?pKtm|$h84i5_pEm)Z!+yyki!-t{3I*=lGTgX5M^8o8Lrlh2#0z;fq;iI4*0a0CV z0R2l$L=+RgyiA0Sela_HDCk-_(A_QIOt&^whYE%4{MXuZAWd3&x&`8duHl_K)$Z2I zjH^o^kUqerdWz!Xb6EcK$HLUK|4mn`@A<}HwkeRlOHv|4{JhUEADCHK;E?g`oz9(x zN<8DT-ZybnAE}4Kg}}`K`3Q7H%Yiguw+*ZWjfZ`G3P756n{AOK=PN5OD$;3k&2MhD zMPvvorDFnVkFGJIqGF&gnX10tkOWK0^{5xweCmG|O(PPx1Qg#=6_q;r5C1On^A*-j01ElbOO-Q1N zp*;xZ=jRvX=f^|~oSzr%ZUe$=`)P4lSQs2G@+_vS*LL93Lv~9<2DPB8)0my>rbVww z)ic^$5Pn#GzO}u*Ca3~Ig{n|uTbstEj-At0s6?AcFrFv0zZ{Ex=9v)8C1Vm5@?XUDt_d+=w z#(F~J{;t7q91~lyI_m0+;Lgd(x(4j0H`f@VZRwhj%7$IQ0fhtB zOdDo$E8!jATzKNPt^#rw5Mp4pC@LdJ1e{6QHM*ZYdlna`4j9JT+S*78fyCtGelwP$ zk`g>L&i_t;2M1yVG@TWvrcZ&+-R!Z;_WZemjLge-3-Q%lg^H=;W+h7ovLl#vRLMV8 zr<#tT%^p9!Ue1Cjj9Bpi0|Wm_?XMs!a%uzz zk7N!Uo%26`mWq(>?eC|@cr7WJ&{GUnB2dTsGetWmy?n!;OpU4OKGy=w*pHvI7=d`9 z51b$z-)TcNKM zXrj*YR8i2RBr}uY6uD5kLb(cAF4rqmq$AKrFanv_*yxGD>gVciEIIBnV27}538OXweO6h_k-bY_M za`!#+m&BvI^V9%4R#I3<{NxGXjn&k+68;)H;{IzXoRNz4nSgez>_7sK^eqGU-Xikp zH31QjWPy}%bvzMtb@k7K;2S`tvT|^&?t9klh?w_Ccz8@u3b_L*rVudq;^M+|OCwn> zockDO2oP16euJuR@<^dF2QjqU-+$VoFDW@TRu1pi`@7c<1NprUtj4w}DK7k&RfYyt z5WS^eU>h-VhVJf4ay^H*TuCqcQu#RJs|3!BeiS(WSJ>`ys`vrUA)qniB*ys{)x`Pb=Ui9>Zx~ghbZLR0o zi;|8-d8P8W)7?d&W!U<0cCFHv@Pf52C=e0x<%?eBn{JTd3<^ore$B?9E*QFKM!l2| zFzYU#t1kx@mrhW7>oznjz>5P~D4Ip(aVU_#CDjWQe9!h&fs_HNgq+;Q(K`DAa~qw! zqTb(gil z??4pdeIMS_+u$7jSP(j8KJVN_4bMynwm)p7U|CP$sm1Qz?hW( zXFWQT8F@RZtVWOPXN)&Clf(1lggun++*c6Y)h6>upSC*Zf+j-a)`Xn4(Zka21A%} z=k{E{9;mBzE4Qf~yV4b_pXs_ep8oyggO#Nvm6pGFY@Qs5ST^!J+`u^{#D7FC;Or}` z)+PW7rgRh)6Fd9@z>ZXO;Ahyz%8lIoUtf5VmC?EhiQb|@zZC>*Um zf6U@v<6MP=`{b7OscLCCY|PxQ@jIhfA9_64X=!OWIAFj^2Do{EO~B515B;~0AP(FI zl9nbzc6;Hx5Jm1kTuMxY?7G#!=QfqdMh<4}e&7KxHD+b0fBzo9=Qe(O;T?F9K;$hq z2L6!-SNs)J@BBY-+EO=wyA>$uK!)g91RI*ZhMpdak)wr0lO=*3wETbFjXZ`_cj1=C z7g&4=45%&CAtGUEiBMY`VWDPZ#njPJ-qK=`l$>mBRYZVR1?HTX!vy~Na@=zkUGz~G zL#>GKrkSwHQ;a~VTt7G$AK$?D?=dtXz`*zb+nL6!yFgL2)#vnP?~kdm*PNrO3+pbV zqyzCr-Fge*Up_&X$$^K_6AF_|Y}v+WwFd{u zQ2bVYXE@*lm5#pt&dj>D{c;MgD6$Rp-U_3!(JPKonGw$H?rx~Arq*@?H~#VF1%mWoBkZI5e815Hyr4Ex>;SG}7m+i@?=-|9(1MfP-{zq$^jizIxGczq;ze1iqT?LA#UK@(n#y2*rqQZZv%O5AWCIFQU zUR?dJJ_ejk0F@SutOH5nbX#Z-NMX8XQX8Yi9}5aPfQsJcTsx?-THXoC4h(#4wRR2w z7AS@sM(03u@jhDPb6)Zcq%kloY-&=Xhcy8qb8Ri@-h;?nJ$-4Lm5~wReH8Lk9G#Su zG%?=ExGhrHdFgj5WcRn-NxuAQmp$+{4v%mHFb1}-I4D$lnfvUz&cED)7GI=IW5-Od zj*AT9P+M-V&BmsP{*m+D!f08UU6|MAOygpiuF>{Xa$oWyB{}&7%^XX$@9Ef9Mv>j` z%5zY0u$`H#oIs`XIa;gtYn}tn&gq7R-<5x){rr%crQ*RyyU!)8AYJjPqujd zOy;(P*dG8rCsoibEDBKo3LxBN)rf%a?tkYYcnX(&w)o($?Evy`Cqc;Wqtb$=M#a-( zWVHjbV{Z>5%o26wFkkb3$5cR@k`ft-BZij@(A82)gykp`BRh1uscwH`thd2&j?1*8 zFDEC5p$|7*3D|i)Ha4zy3%*oE4ZE~~afRTbe z<#t`i#K?m9rq{ujnjd#(>!%QBYj48N-))5^PJgu9H$yq^|Xw6v-EpxrIR)-kg8| zGqCkHN1_?OZtn>WZtf9eb3i)8ap*I)El}JN*$vlscBX19qtpunf!|HmVWimu0oJOX zP7?50fcK>qa0Xsy;sqo*Y_+259V;#_1mHS!@tx&f?O+^ApYwwRe$D^dupIZ~zilA? zqd&SCwMR~RwbUG*l^CnV9*UbAySQ*e8mzUn5<$^@_wJo^R0bZZiFxw+^6}|zK$DN> z#nB|NC8npR1MgK$jSG_&GWV(=t>$`WQO`H)@z@})TGFYs4`Qo|Jmc7l?_=HF2)}+z+ zI4~TZ_9fQ<-&M96xrWF`<0d}3hS%kEzTBfF{^iQohpc$<6UCdve|r)!SJ%Fq>j&Mu zZP)dkQ&Y~fEt4L*pTi}dJ)C2S)zz!V#rUmyOdyT-E0%s1poWwe-}5UgRStF@J?Z0p zF?hO{q&Um{813<2-?*+l_g`l;K8fd7>8KsBmMTu}@g;Bkr%{^h_?2WeaE@S6tW|<+ z3L}y)-d$c7$j!=f>WoZ(ogGbs3B0RMC@5|nCHB)Lq9>@7xo>oWtQQk3`ljNzp78-PWC;OV@&l$htqOyB$GPvKsH-E4@FxH33lXma z9{w}L`iLRfo_@aA4m@kf^7`UT^ytdGR|`BGWOm=cz$7T*;k{g=2 zGMD~vrXn~F_b2m8OV#UED**ljN-#N}_J7$%6ThnnfNBpK_61<^!U)8`!FkI491kP#BZ>~??&MU)E0I}G$f zVA(To>h>JS!7FU+zegeNOpuY1vKzI`^xFf6=gx3`xJ`eJ4mi1hg{==#Gc`38A6w$J z7l4U%b#)~X_@J?hi;K(s9W3_Wbir!mb^hq|2ILxgf8^-`xMj3<#|v-Hug@g%aSYp@ zvrTWVuNyU=w3V>_{NYd!Y`yj6p-YZL-6keR40Lp$`y+q>oac9W0$MDGa*rzDv$*$Q z%D9P;1uh<|06Y*h80=7OQyYMW5_E@h4N9v6r2!onU8?;%zZP)4sXBUw(UA@8jJ;i z;|rC8N6%;jdFEK^R!Ggvy%$14x|WvW0Rh0qRt`#oxrGIHGG<8$aLIwLv)ril1mIC% zR4gmo`u6Q7u+jnhQOAJV|JCDtcD_wmsFchV+HP`jc4lF2ZfR!bFbB_VF?eAeTjO~T zM;cfbjCW?SQD?f1#0)k#11y$BNa1tGR&b zm@5IaAy~llpDse_eBmnN@9(dtcM(cZ2u`;o!ZY%p!@vRH@h5Z`3*B*+@(YE)7dd+w7kd@!AdZ-oRI!xtLK&4}9A%9zD>{)IQRTSUIM(Tb z!OHyjITk2;5Ll^U_o%>W6@X$j{AZp@_x@@h^T!@pKjO3hvO18S934&a#J#4&{@-SI z{P^w38djboUzNc>lHn`lBx~A=|6#TQdtvea$!z7^_w7f}Avl@iyuV=1%d3eKEY33o z*#q|cE}sQ(*h2K?LL}LMg3HV-x1OW@(0{##rXRAWO~b+AXllB>v4KxcehRD^>$p0* zCt#nN7#Z<9EWptQM#6`#g2=&2`eVc!LqFDX<70=sip)714}e3pcIP^kGqxxQUHN&A zuCm210h!nJKyKLTw_}1J(oYN=z`Iy4#3o9bjTIe7IWY5vQ(RT87x#QZ^pFO>64O zNr8cZ%nQ-V|Jl4&N*Vn94GGCwb8~aG&hvM3eBDMZAx~Eu7o7MlEViFKPqKBKn;UWA zJ8%1(kx>EM&|qT+H*&kM(bOm_v!seD1m{cquT!?R(m-{Opb$u-!Qe}!@4qvSeuZ99 z>fOL0$?N|l?+QIpZLC5!&n7eda8J4oeN9J39 z_g>W{i|v({q7@Wm19?m(>OTcac4K35L4gIpO=5TRl{UuqJb~r249}?gr`Z&h;G& z@0n-b=broBzVCaU+2*g2!E0UXyw2-5_ha9G2giNzX~LN;`ss#&{dchS@2-v84rCKe zKGT6f*3s_Dqp{Phs>jPi1!aAX(v8?bB>hvX*R?0Ps6o83vb7z}LD|1-g@uKgn0ol0 zS9VF3fBiaby~ILIV|lo}Ff++8*lsoqF{(z6AF%oY1Lwe8mbF*C#H8xqR5`sSG@V7K zxwvTP$XzugA%MiB+| zwA(4ToME|OXOR*Z_NLZ1gs4{H8=8%0fjso!!2|Xvb(1G8A*h_nF=laJSpECwuOS-s z?0Yn1CD0$R^W_25&4g$+)Y6+sumdP%qJRrRF(v1s()EsvaU<<2aRk9XD@>yFS) zn^tJNlOEAwgU8+3xoUOnWC`?5Ffb=nYPl7e2?(J1)H5=hh;U(o#sPJKc8t1bOm)@* zO}?|YEy*38Dvi>BMu^`8&w&!OyMkQlD|0?+-)W!@Qth>$J{)zb?#D|Y=G|Mhfi4Za zX8QK4_rZ$j3C)tlrKR>)PejFr{bw~Ur~6_p_a%|WkUuKa^| zvhA8F4)#vyq{|!}DtM6Prz^v@A|aW?N0E|u9;h8WiGu(ObJFY_4usq`E{r>$oSsO< z_1T-2ZwimQ`fnX?>|nervqd&(J1-WIBq}$9GZ++EEu&#FhRxUf{MukIh39h-Ck6gd zR95D695`pT?)$gzXw9FD#?jF+m0%qz3f0H$)p1pJ=x){bnDn2CwlwGGTRk^~gO`|y z2p*^1ukY_Lzz&|cf58QO!BCEaN*@UdmK`z5C8}`hJd#~%wEza|!KhTd_Ejd_m7;NH zviej1Khi?_KWArmY)sXA+8PY^O>Zwu!BwZF?5|0bLm2~|6FnWB7yOR)UN`}b`FVL6 z-D;?&R2ZzMN`G*IjXHcXbDeJSoK7uz7oVOv{_pWIfXS$;j@_eeLvIms@ zAb3Ee3qnOIMW%=BbYmvV01z6z64ajco+AvhKZQp9g^Mmtki2QY2wb08=U+zp|IxQ0zJK*@*EE^txsN%5)*Gis2Fl!aE=1c zrvb=x=}WR3PsSmE0;P)ilmKIxC@A-gdiJsvK7Ok0-U{jl%D%$Ucv^jQjWc3GLJv6Y zVxJMdlxVj*-v0!zqzWgV#dp`-h~SmZ7LfW%Tf5k1WpQ%S|2BndVtd}7n{HeVPZeAn z1gQt}^l)}whl8dI0k+k#QPahqM%ZH@OO!7EsJp8RB>M4>9|*BNWvZ62fEO7ms*uNO zah0I{{x>`BGOmY5mp&6D7(l&;NB<`m4ernSg;9LH_T3xk6}#)NXKqHB3?KhwWPtQE zJDV8!6;0$R9|p!3IlTu3e+wSPh%>-G2}end;y2N0X`Y}LVe))~m3-w|T8-Ob-M)Zf z8Z408!6Bs}Il_@#j%0w`U>pKjQ0STs?^<=!Q4j?n1Ihy~=n7EeU$=FTYSa!?=~EF_DmK!5$B3EaKa)y&Bkx!niw z{y#af1%-PMWLE)!4{$&G`}?Q14%%Y`=~Rnlj|INMvt;i;XKfj=xVF|jLFrfZdjH_W zexb|7zCtA1Y@8R@yU?)LLQD)D*IQtQi@IPH57)lp+h-oA;RCv#g1vWp!BV1~?amxi zB=_hGXd&=jHrk-E;2FD~Bo@V<(C74mxU(sU2i)d5L-|x9A&*;{nhxh;A}04dIT4%N z-`~~TCjY#*{>I^O`(Uz`1|BT&oM^nP@~(WsAkKh1XQ&8!wED8%U3h?oVRe&ktk~SB zn>^-hyNQR!%xo1LL?BjcV^i330sGP=1W%O-=*FNKLJFeD-Mm@44Ujfd%U zE<}KMadM=j3UrLf$U3;nFynD}1R(wwn@bn8R!XCgSyCd_+}0NNl!O2>)O&flt+wW2 z-5J#c4f1?D;mZCd3U-8QFjjxF2Q*w>u@n5RR)5*%aJh} zzJY;`^BtY2NQmHVINS1^W?AvTT9S~EP*NIdTnaDWAshF8d3r27#!63bFqqp5HhPdh zNU^SJX{xtHEs1TMY{+Tk@rl2DX>)&lSDkvM$dd`SDgj1@G`?}+bNhv|4pbD5A4thQJKopG`&)SK|#MQn(R20TxrSD>H zzOh!dc4Sx+a84T?02Ob|x_VLr)YOv6DUyvx51V(EkWAJ#vG3xXUN;50)YJXJBylg{ zcc6??6aQtzIBPK&ekZIH>+1{VusxJhlTlV?Hy*gRD43nM z6z=aolwAU$A6V&l4BEcY(Wa1bx7&AfyX*tGS1(rme~y6ZjHfHmfzLvLLk+^%( z-h-Hx@xXdQW#wv`oKa)X|26$(Jx;_YAkXEOo4eT}4)-(IMN@M#2Zs*@665-HYg=1vGe`jyS}(QZ&_zQfdkh=_tA(zx z$?COnA6Qg}n=?jdn@1izZ9{{Dh6V=ysi4=x5`d7dlB6VbM@kS|EUq`jB3Rs;Dr15) z%gZ@Qp6*Lfm-suXOM_?z`@ujFov)@)>RgHQzZ$!lgO{kQtBGJkhkwt%BZQydG`oDc z9`3X+9>h$BOSVFKdh&r@QBrD83TtP)aoHJVYA?S1 z0|TyTv~;ky)QlPQd;mLOpjQM#Z_empN@Pg&D_UhHBq;I_3f`eide6z)@u#g-ft zUI>Edl>3Kh%xmoB`kjs!E}WpT%^n}1j3lT6J0;~QBV%*BnV!9UY5G!+J$@R*$DkmY zp4N6Hgd9Ay{BC1mU3K*h2v8}0a|+E6P`%(-)HLyibWO!OZ_AEcW}+vRQFim$OnCsg zSVvdaP=$jTR}&>$JG!+tYB}heR`x!dsr9kDL(s_sIEjd_1vd{X$=>&0y{e=u$p>`X*Er9xCCsN-zo8%rr4u zY|_ZdVY?Ehy&#EMQwf3_!V+OU$-h&Q*O!INF&iJ7tgSjN%{J;crJhsEeq|~x-385V zmsDB}c8d%|=1v3Z7M&Y6iGAMG(n5aaIcRle%?>Ek^Kr_3@v=D;hcYR(RjD&olWowuHa%KTpkGf2)R%%6jW%9sMxO>MR!lr4L zNC6uOikYtN(4aLJ%CPjef*rjmC}^ggCkv(N!<$v!`xz)K@D}Lr==k{2?pY>8y5VgI zzK=9(4SFD=czP~Fjnd;qbx;l~610t~lU<`;HRwRvAzBA2j(m8KoC<53(wr87mSr8XxjuiYDug6E*eU zzR^~c>!zfqr>CYur3T_B)syv`L0aKqVek&5V%crooZ)eNXn@aY*K-!|M1ZOYUR^0DY&m6~k)ovVl>JMF*arsS=u z@1U;LalEpZf_kdn2l_aD_3!SE3=9;g^il|JwWM|3-1DA*|E^of?(_jZZy%;Zks}2+)S?`}A1F zJJ>yQ1JV!a!HhS-CIa!D5i93K;|w=b8Nf4cIL@Occ6simcW(9owYr}5j{ zH#QCj2A*ENEXa^~kEvmTCPX0|t2e7WH`PGp%>SZ-F8hz2;;*Mu`TzfEM$~@~YvV2M z=N>4bE?qzOnS0H|{(*^{oE#iYkTo7Yus_y+VM!41_8nAbk|Y7?z?G1JkugH(gh>d| zsIFqguBM@3)`m|<5aSKqGayHt8qNNG5nQDHt0xuZx3LgSM@-B>Oe|gg?jIe9&u+fY z%j;eqjGkrzkWo|=kGI@Y=i}*WH$l?mz9A!*4b3&17184p+tx6~bSH-`C_lC8#ovEW zzisvkRNaXQLru;2l!m)m+v}mAqsR3T7a1Eo(W={iti}!@S0Fygi}^>={EnsTwySsK z>Dj{Bj+(j&Xg6ayS|R&!)gg<*EuH4;133xMp@x@w6?8UU{UUnzOwveAiZ@w7JROc= zl%^7iT!VM(`3snP^PLq&{cV%a5^dL_ zp+l0N{RCdfpu+M%j~#9?uHi)#!tB{r_>Rb5LF-y6VR%XZvuP3Q;w^mq$+H+Y<$raj zNZ0tul06+(IG#RLFMd-}CxdtI-v0ivaFPz4eki}Th2+qHD8y2iZ zVWp*|aQ;av$b1RVq7d2*jO%4=23^}^BJy|c+=14WR4UC?upQjJdl&caU12mdA|i^$ zS||Z~O3gB6G76%x?@UAA*5I?{zBG>Wnl7#MiP^_K-=I=-~@aD9T$ z%~kvh0Z(I%7E6rA>{OnRLeXZ9L`I6LHR`!&rIF0ZHXl!qn5E0=qpWODs>3na6GDwY0t&ZZoy!k$$Tie3C!PigvVVE#ip5nK+ zZ^xm7w_#WA>uYJnUZEw3M=fajkIqr3rw_JhVs`(%N|U92^zv4exLJ3d-RekF!z0>d z@)8EFg5Jh}`XRNNxc zvO8%hvd)#LB@j-LdLH{>)@G8-ix=*yqWMc{YZ?oA8Sl%?^vTI4ibv3^fX=!`US{~$=vq=^5W4>%QlAuP zc|X7>uVL=}Z^>KZ_^XIL-0nw@9udBhfcD=9j0exr{!LkL!1=_|b078Y2v>{6B0x@n zLx4mOgU9JmROvo)3w^B0XKkSKL#?LJykby9gu+Unuf5?35Cl-x%py}Qu3a##r?EHv zXw9e|Qza_Nj8C8;z}a3Mom#7+b8am)OtVi?gnDZdcydWeN#N~h?a7Y{d?)-DkBGsJ z@Ioi}N#o-mUUz>UAJ@5H#f|JvPI9mIa zC|cxO0pgVl?_e)@5_Sy?>HrnOhP@4Y5FbM@(tcC>iARl6)oDG8i8A9o=F2bq{4kHU z7kW!9>v36qBb9kT1pfI|460Bl3`NQF#E)Q49_-3Jf&LO~>m`h94MN?*3^SjanwprH z^-PlvEL^~geEh(d&!;BsBdWrY}LiQ&!9hC*ouDS*fLfb z1NL2a_mZF>xtb(Y0mp=8u%KJsqqG3VN-+>As*uSz!Y^-PS@iYy z&xL3>zJGoTNC^p@(BT-aLJ|g1m7~sxadE3FTs{IMreozNHJD4Fypf9sk)s|qQmhQs zgQ@KWnIy3YAkh5)Jc7$(Gt@~$RDjLE;OMiYRm*EunbK>Gt z`;MpG$?aTRbB+ePE2YGjyA@<*f&WogTcMPz?Y=KY_0LXNQ(t+)zcGsvO8#UPk)CCK zEQu*7Y`5p%f!o<^tybHX8Ax5;c>VWralhcP=D@;D1((+|F4Xq=mI>FGcE2e8h1Xp; zb;3n^T}JgfYHKT`S2ry-Ha0$<;nmv; zut~ugcx!7bg57o;vj@dHKld_W(E0>&T5>cbV7h|7H$3s@c@f}R_Qqp%b3@*_a#1X* zVC~xCne4Kd!G`vmtV%c)uJ&jA$LW7eGjQ`o{G(G8it@eX!55~IXBtyzyKPU0Byy!l z1M&ss2rqbjQbn6WJm#{8Gx#?saKoTM_OhW7ECFtAd>_~PLzLeMU+>HE{>sLH`m7CC zlw9Hg-w75`McTgO1Fzq7KQL1eT_I*+`J!45EAKsUHUC0|G$b<*?uW3DsjNL8N$c|5RpvpVT!d zMIJuPBqt{addG$^rq3XlOCjDsm-PgUW4qD)acYz{_a)D>OpN(_jP*EBUXE$D$b0hF>`KYoZ(-T^<*X@j3u zdvryt-5A}9NNvb2zyL=In%BT|1HQ7GX61Gpu5o-GqM=U`V`C*Z5sCOT#3lTWm9an( zVYAe=S?q}eFCTclfc_%MnfveOg+99l)VA}z=!8EJHuTLINjSvDP=x(QnJc4yxibN{ zkot;>lnA$nSQh@O<@P(S+X;96b9E~~b&;2nvH>dZ8Q>0~iejguG4RNN-|DQSe*RFN z@aDzKNTJgepzYBAb1j^PQV+P9DCegk?8V8cyLF($%^hK7Z4F%aAC(3E*=AN&qSDe9 zRnAVec-{K>a3eo>2*TKj=`CSK`LBIgRFn&RIXONEYNxbW8B&v# zjSLF|L=_{LnR~4bGcK3AApb!4l-ZgQ^4=5|4`{3f4OBQr1!M**cBj473J^J=-wG5= zY5CcFgKp*WKY)mMWuJ?7`L0j>^{Z?^-Q=*z_d6~Bhgz1I`Jb!A_F=oD4YVu1K4g0A{@8swR>l}>Ja43*ce;mrx zsC3AAwlB6^LJ^~&*ui5qJc|T8&dTcQ;yPNnTA`l8I|FMpYYsb$JJ9+7Gor88 z$sy!Q8xaufaEmDtFM&N1907ISDwDNvRp344pOJ*Z)bkTUPFni;<4wErpFc@&2e{On zp{0Ee>x7x0(sq^PLPX{GSmcVQY4~sTHsT;@m%gHWoK8Z!!SZL9cqwmjzYL zLOYFECbxpLG+-s4&osdHZf|c7mK-?+axyXzzcU@ccj6ftKs_BD%iZ`~HgGoumkdv; zA*#3USh>HqOdgk!L3ZQ^oT|R|If3(`(0q!Wi;D|dY+_=q9UVsK7V1=%a^B(L;qM8v)BY+}&3qkE zPx~_xgNDb7kY2O2j5q{Cy9IQ50q@n_mbtVs6=O=jU}X=^060R_@-(tu{mrX{J@?z* zEqFY*34CSJfti>_@_J`m!V9sp*c|^85HkEuNHZh7K{uFn01z0^_^tjHF$n>f4bv^>zF6ZL zTLKUcs7sIrp`o#yZHqE^726JcPjDjrcaqcT@j+uqn~2UhU8?mstW#J$_PfhtplCy@ zfw>2f*n)l5o4yo~+5v!P@OyiCiBO75IR3ppbxA!~UO57xTuCuhMM;VIkk|DketK57G;q`y|4rm#3M&KtIpv@VA0^p}fNU&ql08L-n7HSX?Y=w#eCk zE8Sid7!;EZ6s-|@3#b4ZGARkoTOeWoVUQONosoWugF|~l=SL9gkn1(2Kmz?F^Uu9s z(T#H1-4iV8BlQvs>mTpU!I=ezhtxG1F+3seO|TtOqH?5!ycQo^|vnp?1S_Z)b7;%x3>@ zA|=N~W8*X0gp22yEF%pHg7z?OR1`X76MOMGa)^4%VtiY|wl?Pf@gn)Xmw4ey!4=nR zgJki?No*bGT^50(_h9&ND;#yk{b!t+)L5Yt)gV7ZDDrMyTfG>~*vM0RRtWB~VT z`M!L05fnrXxn{o}i!tDO@9s#HBI~2GCbw)aR(z|kq!!-f<=JzJkbSB<=so=zYvXXv zY*X3$3el8j&aQw43Jm`;+zF4byHESJp+A<>g8gYyvzJ*A8j;aBbRRmF&j%?GY28t} z(I2xq<`^iRImnP@x%TDJp}jknHyuq@e)5sdWct$h3s$0xV&|jX<&JoFG^5$sb^DQC zW_3g&l^@fQbED1LXfZ zlX1HHwIo_64?dUyrT0cFd_Aqg7pzV^5g}{zNosuBNlN;I&mrp+NNXyi$KPiz(yZcr zh5r7@+&(sIu@1I}%c9QfZ$mv{u8VKDn$PFMj_4SI8@r@1P{Wk`t}aP{+WeDyOtKAA zui|poSYbtQB>u`lM7dhBx)f5ayntJ8fvI`yf%Q{k0eN+UUeXAgMV#ZyzAly`&5t5a z)4~mh)oHXHkhi=}@ArtA{UFq0{_>-HV5GN->vSE-vsTa}rM#j$nw5v)#_O_8tyfc- z+{>`W*)d?PJ#6!#Y4AWNq$Z$<;M45&_l)&nndU`Atn0W&i_E*-Z^PbpXF0Ro<;#(@ zve$9J_WL+F8`varlb@9)L#O*2?QjZJ4$`E%dMmS*TsBxll8C8q$~dw+wJb$~ zVcOrn71c|~JZMMSkG4}9&Ys`eZplGVwQ0H%O+hU6h-`=IA=XQ>g zG3vembN;%|Nyt*akMHiEGYWI)EHt6CU?*x@tW==82QvhIts7?8J* ztg?ODab+l_7C9iwj_f9o;34qnFyw#P5wxel{)2#L`-ws7B#a(-%Ld@!phAQ#QGdF{ zowkD^M(laFSv9;TbB3?4B7bCExRL%@R-H4kqW!pS&WvK`O%t!Fmrh1u**TYah&1gK<% zdz51=wVl@7T&fnBW`5LiG!EajF)&JbzR{Grw`BfDN>Y)wCR1FK-^bu242iV@S`?;K|c6eqKFPA#u&~Aa!s= z?hhY-#$vCe{A7!kM43k-2iRW6#J9UeiW6rfG%c6H)7R(1whCX6>I8n(HAj9DUDB7o zZw$eAGuQhOR7H06HTlNs!sNDLE`iSoBd4?llEU+4qt}cp_l!pfTOXOtWy-9L}+qIC7eiS`}y)jOD_w??ua!|r6 z^~8ib@Ryjbb#D~~2n#o39-qj65npX zXvdaBT{~!#rYjdQ{6&B!_nt-^zqbP3oh3t0t%z1jjfuw;r5UyL#(CiaVbVpn28lCPjHl5+%YJkjCa9D=7@`3lW~S(cu{7pFCa3=W2;iM{$_PA@#{D5n^$pSOf-*du%F7q-6!71995wbZ9+pSO8&V!Fh)R|9_ulC1rN{Z zqM6%Jq3oFTp*8)UOn@Ap&2?D?DMIa_>7iIfg#eHIF@~(Iwg9@K2cISti!ijY<9 z*n@}4$Z?p)$F{?lbGE*Op8sooL2PO8Ad#nad7imXRwLb_1#COdro<)i?H)g#pR06u zuI|YX_*_>t?9Hu}mm`s&(|KLRsGRNr?r(0$`sfLNFFkh#n)RV4WvEq4I>zSiz z;!R97v0YZyfs+yMp9>_SefC5954{cIs>9cePmjgMB7RDcrAcW?Xwv}XaD|CZF!_GI zD{jcA=vCc39}VnTI%=f`~f= zs6I1AZPW)L8K;$=68JpZC`m^m5Y514wqtULDhsTU`+5U<& zl1+liw{LY5iN-|<_V*$TYLF`@LBXvet&_WJ-)PiH1e4LTd&^Ep6P_y`A#K+Dy=Xu8 z{`3-2U1TZbbye876eY@Bx&7Q|)1!ijYnu$8h$c&RD(??Au?8)xvIw89`bSl-Sym%g zw!NC2z0;OzC=G;Vrz)ijPv@rR^Sg4dfzL*zf>p>pIkV2ilNzt|(NK|;*Hg}8Iv$U- zfCoZ+7ABJ4Xm0V+?zrr4I-Rbim#TyvA9d;vp+4H!A^N3pYKEN*X~`BIr$^2QZIlSK z(b5d|o%UF!RCoqOF9fOhbOkBiNT)`QFJG}AEU1_jR-k#r9GpbmZI+V()Awu)xt85B z%hxpH*h;JXs0CkV=y}K=MlzfxAIVa!Tt#I3@Egx7EpyMIp32+6Vttsz^RZ2R!ElBt z7MSy{m4%b>R4ZjC$X6p0!<@pl%LX0kd|vIgW>{q9o4Y1G@L{?oU;Fd4_XjbN9d=cA zUHiVI>+|-i{hvW192c~IEapGI%zt&h)aI9`sdGEI%xdVgcTFe=? z@U@idvWk(!Mh&h@RAGT1Tq3VHWo5JtF_n=(MAMF<(dne(BS}qd>UDA@+6Y4`b<)d` z7u68Rn$`M(hir#xmEE2ZUdj196$LS#{y*yJ<9{~26@+j0_C=yQpHy8+r??t_IQZB0 zT~dQx*kxSU@%WI(@{0)1q@Nz*P)H+QKGYHdb zJb4+&PV@>`xJr^OA+vdeCqY|e(uI{5yZ*K7TkJS{3W?|-$!r8lg>{ZS8$ykfgIHyd z9JG>u-DDVGHpF86@EP?DwxL$1FYdLj=FMh-&W`h6;dT}!c?gNjv^~aYqR}>bir#>s z%Jvr3`O^*)-BvwsFmFVIyGW_#{gN!Zn1?&MU z9a%s`lm>gega|Ai(MxRBI_{k(-sCr!DslakO`H|a!S6K#=-{9<;Y(`uYR!Gw= zM{Ttmmv&s#$eAt|Tb%0$!(#*OUjwE){x$FGe?Hu+M*oLpL|EAB zV4l};8F;=1vgu>Y65`@chs~-$-9c>K4GIo+-r+<^73ZEYI>bZFZ0=8Zy3W;zPW}9> z(BCwx{oR-pT9raO2%uc#g;O<*5r9S|kUCI)`**p5AnTyUQ|arACjFT2On^8yDrg_H zp`|tD>M9O)6d9vWX-Qyd0Kxz2RW=B-$t)%VSx6rmfDn!^$Y+94Ek8fWfJj!G3JK*% z=4Jpp2e4U|!*BSx1b_Jc>eL6AYLT2A5fXy)@buTOY7&4P-H{k7Eyw|SH&_}#O##O) z*t+49eX&2AlmJkC3Tp7GgTWA5OnbZPkQjk1aSkwl-rtyd^ArNm;9%`(45$_tmw~4H zX}`vtATarJ_$Pa6y+XF^5&@Y6f`s@+4@B2@SJ6{aZe}X)Ol7~;0_7bn3=u4KVB`jp z(EI)@4VTJ<43qwcT%{)CtE|w;%}_Z(yD^WP@}Z%M&Ih4bt?U?j8sHBd8PVN5aCAXx zZ13z)uAUu{*U@cHu&%iS>isWne(jRO-s-~A666{OA@zkCQ55IKTzdgLS}2-2^0CHg zdY#GHkb0YY5qzYQ$lgdYU3PqC^g*o9y!2=JCDgHsICeA;)x{RC|i**36# zh?g91~db||Hofjth{ z9B4+!VlW%_TtUrGGH&JVkK+@Rh`|6_jVuP(L63513u3_Gxjo;x0!VbofvYmUdg}(H zh$2%PS@AQn`8o`P12X|o1)w<9J*pvHs0MN?}^Q)^sur1oFr zeFvE;EIHtwm}zU@!`TaNN;NV527TTb&OtWH`8NQj;Pco8n{DU9j)5z%Bm#prAk%C# zDlH+QpsvnMMC3R==an;BD<2*oK z>MPbUHJ!0Mhm9>4{|vo*bq1{>P2Ub6=YO(Qj!^B12>_OVanhjK+`&?ztGk3>J*P_JxJ3GxSaKUXa45BK@irbntKrO4+%j+djW$YW=o*J&#$hQYSp&^?*m*kFkHmry<_R9 z88r;4p)ea+8!1un!4k4#)yV+YLc<)JCRkV?l`6yY{J)z?gVegZyWOA11#}mKS~prn zz?<3AX#l7W6=`YddMwlpI_H>2=lHdI!I3-Wxa`k1{PRYKNyDOruJrnE8qmUm)*pw( z^c!-SxQAr^b--8~E-+GIPKUVBSY_fN_if0{#<-{>VIEzghypftZ--9${~Kg}DV4Y4M}EHaV3p7HiA|H#RQU`|lKBC!F?P=TeZ zlB4-4I5;UcH#d3ihSrok_@z>op($Hxy_7UCz}g&wTu-DR0^$(}mSFnH_;$KdCvfv8 zxZrri>p3Ol z%BLVc0IRyzYkGg@I?m&xqqQ{Vg2itd;Qj5Inrx+uL5h>6Qovi)4FEnWp0nv+0#hkkC?iC2A!Nv&D^|3J#3kzxr7SxA3 z=PQQlj*CU1fb#HJ1s?t@-9Sk8*GvBQoQiXATXi_Jom(~3;$V|M(r=C4m0yyi9!xjalveGrOscVda-C2=yw}v0&jZgD0 zy9~S~^VjJLETV@h`ZqLRpb>?~d?s^=+S1gnvg(1$PO{5KUAKrDE++E%rI;ce94E3z zwIVcpOXd>eb_mnnnkPIZzm`H^0D#OG-Tiewmu9KiAeT>9)Vb;A)J%IiDL(l%qFWy; zt5msaND-yzr*xw>u$P!xvXx}i_D#U@*129pJ=6K zGiWmWC>%@LlqsrdssyBO7Sb`n#}@Mh!`?CTS7W1|&lj#l3vKGDKY)5#6!t1{P&q21 z*~x)~`*`p2>G+DgX)9|gmkYe! ztDLs0(pRRVcQX0Szu|&Z*5mqH7druj!ac*5!9nnCBB3WK9?|fbV}qyO^q%DC6Ji&AC56Q`2IDo%AkM*VK-9F1nB?es@B8k4WfAXiA39G8A= zhqeIR`Vp3A9g=%!!oyi_E;`wtzBib9${7!V^zjJkS1rOf7}I;*zFbstb?>$jmqo_Z zAAmS6uwKHiWZmpdR79}USV^ipXug{&(G7K#K!!sR?m@HUM*Ljj!ZZQ4tLLcGZojf( zBchL~^Io3O&vcO-90hf`o>!P#_rHBjwR<(4v)LuVmw8{bguYDR{Tkv8VyD3Lp&Ht$ ze$2ywun=G5(|O?JI*mQZ($kAyTP@Wv5>E+1;o4{RVKhO)iO1-G^W2Xv8k^Oq%f}sb zTS;T&(yt(3_c{DdZH~#aa zi=!!a680d%itI`}VaYS`=*(^Nc#346x*Mw9F@#sX$j@jxOY6jHE)5Aq*A^ENeS4bs zm9k>~FbVJZyX>WWaXFuuW6GVT@Fn-u7cUlSH};4IG_RR%YtS9k9#c@n z;3Q!y`Iyk1`1Pu!4whOwTOGq-O$h{|{6|lG3W-DmrMu=Q!#ggluYrUQJ4^+hB*GH^ z=`m%=OETfqAej2W#J&PUDhzlHOdZvGCMZO8l7c^du)Ba3VBWoEI?Dt{Ox? z&p`0{Tv|k{u{b?a4yUaB)q~< zZk$Hr(S(USpGmmEE8WXUW9Zmqnf~y@hV5Di%7LHGZIF`z$ zQ5_7Zd1ura7B6dkM!me4lApbhaud6G5oQ)x>j+_l2qZV=%{1!s1LgT^V(9jh7sIkL zMNZer9wp|+GpIYyM3Jd@CmO~QS6Y+IkWI4+$X`l4A8dGGWY?Sd+7H(7$ zY`*6Ht-k0NU_J`LLub`66lFrCh~RZ*eDb&NsW)ixRA;_vght;(?KDFF$g(Gn`xsFp z*Ef+;C@c2y<>c%el|A~qav!hxUvGRQl3PM0(e;?N>^T!RnOLo1QRq$$-GQPuvyG5V z<6Npn9)FLyGUe4~cjXl(uE*juG?NtEFH851F8F0XzOcLWv{<8wCi!9a)sJ#l-p?e? zY|72vh;_kl)nJW3oDN&eagTQm{GsA(i(ZB+`$gp7UdlkJ%_@EKDnX2j*^jXeuY+k3 z>Q88LY@rX&=I+IYeU>_Sij^=>>=9>&?k&!^I!Haii-jPFj8RRA&+1Amd;R0&=f1;L z@)KoCWlFybvM@E&IU>Pm@Okv;{FZ-?tWv3bshnU}Ki+|`b%1jnbCPobRNL=2GumrHPSS7&v2_%tl*2L5OA^*RW0UM zjg}iPg;(GZd70Opl7vaKEdlT zBj=)P+`XyR3GEdC_sF;vJTH?m;sU<2OW4ThHpBbky7ou%KM7o4a$P`Jn&7!pjuBW%!H z^AW#Hnk~c7*aqezl{#2IsM5O=P(ykdfy|qcZ>e8N{3-eB^N|UuTjQ^pE$;Ue>f?59 z5hAHeAuFFBbLpNO;c>G4B0U^w&NeA(bmnlOtJIzwgJ~>YHEGVZVYTM{<4J;gOg5i@ zFKn?G5YS$a_W2&(ljo#Z^r1IgV|B`#Zj3+Rbz=YKP)2a`qDco7q4G11-PZOffw9uU zKS6qR&3lvlx^?0cr<|$gv_iE&+z5bW_v*M&&Jroa1nDJo&s^iku-vA zE^P9Z{cHDZm>_Y$zSLeaS8FC*f_IasJ(!@(=wcL6$RX`YSDF#L7@1!#xjZM)&zD%s zN0ocNxBe5H-tU3;9cF9~vj_37zZH&ee9S(dMU5)h-zE{!Jn)Sbg2^1R+d<HCC{2`}T5o!e2=^rU1J}?c3PX+$kbI)!Ye$hH}TAh1V z7q^6i`fva9!$2#oq*D37(GPbXyq&WvKevA3w?jc4!E!P^3v;F|{}8)yhC%Nz7Q#-k zb7<$Xoc%es)E;#`n#tgN=gsBi(p<%FhbzMX$)r(e^Kf?ua2xAO-Cr~TNoTm@*aI|b zkb&^Obj>YcV-LdViC@O@&Acs`@L4Z%q3`HmcjGs(3IfSc_2x0uPty*apOpViPf0!(O35<6B zBLe>jjJ!#}YXLxR=Tvf`e{!!Hwgzd#golX+@Y@y{>A~1QqqayTP#a*V{6h{KVy11} z+P<@OxE3KRA$#pQO~NfQvf4Bn(7|ej{-eFqU+6z+Y><9)b>sW(6TWnw_sx3D^f<_& zlSi^(9faw4elMqInDu^K3<0?dlEB4=ETz_YFpB_E(s29xySnlV3)8~Wva*oPbSmI1 zottKwfhaQ&Mg|54rA33@iYL=4Gr0`r$~}*J17srLS;Dx~Uy}?<;^JQjufXiWXK$-O zE-pKw1sZ6+t`jHwj!W zLy+akll6^*gXO_06pbI|KF>7c6oFwe@i%h2tpD7*Yxl!0UR+kJuy=oIdW%Qub&ld6 ze^hz6k4|pb0eyDf${s?2+S2N06FFnJoz&&)+MAj%Zg93}=KM_cFEBOvmYQk~zqzG_ z_Hl><()m4%I8VbH7~XT)U48*%v2=wTa6$LlyEv75jQtyGPBTp#?3ke5Le>N{W>~1r zEiJkX*Z@1e;u^}JgXLAby|K~S)YR6{0D)scM9C}2u~L)yM*`3q;A6nehwvq&$v~Kl zerhfZyz(S5BL+E(%&!LS%04XKal31ottwJO>=qzb5G$q@_}>; zS{7L%F4~L;Ob^9*q`W#av%0{h)koVtKOX{2c9a`l2KI?I&72=KYXM%cj0w0M^?^G{ zz-6yG)oE8Qy%{ISC7lYBID5OhuV25OQOZC?751B>-}ui4*4sYVevxM*CH~|o)cc-( z@&q^e+8EdA;nH(w=XY*=NN_E}M0AMKDci;{=){+mEdi|qtnq;K0SfLztWATCl2E&g z$8&I3`!j}9zO}#@>JXrR0t$suw*_+8phA6rdwP*Kk`s}?xC>o}$~&lW$a6DtbqYH# z6CZPMP>{_WgflNKNf9@u`Ws0DRG#o=XlVXb3~nh^)#LSvgzw+oCOtElfui3K6et)x za){c1*PQ~=mK4m*KfNSnc+$bjYyv>3{TLTht2I*?;Z3%>N2c@_rOLm;t*6IuejAlp znzDx=2P%LBbA%c1-H_?FArRz#V{B3ed`LddE5p^*8-QnlPDSmjo|~b-9XR7h-IJTz zAWQCRJlfj;Wxt^E-t^XQcsooU1<&9k#cb{Q4yoJZqJx#AfL6`QEJ+iLhS6u!eP(}g zpOmesA>i>AX|&chmD+^lc^l)Pwl`FyG8q5DUEimWicbf~b`7MS&kb%5wd9L(`0;Bk z4#&&NCZ+1)O0|aWerEa9Bu%VROnOwi<1pW{_Jv;6!>D9&$8p=kA-Mg(v289=V$P{; z&JyFCBkucavao|fu4uL$ISb+?xSJOi?kgz`UdI{Q)Honhw6V$b^vo^XB@_}8V$4g0 z^>w)RV0&QTGDIU`GKF2jJuUE%(#X?3cq-{ttC=%seEoVixFvu7Jj9t250@o4L*Gwq zg10_6D2V>ay%(q2Pkvs;?0cOP3f+weRu>gzm@Im?O+#^ggoVSBTvb3ocd1Y8Wg#2p@EnBxf=%tQsC_bLmLhck@ksa z^E(|^)v6kBTy|D7z&2{KIY;0!^2SCz(06^0@>2EbUKI&UlBldKgZ5gU-luVws+wU3 zy#yT$0t|?OMn^9V7?a_8*CGAuU~d9tm6NL&Z>Qx+9kAxZk{^E( z1+WPCAX-`pM|((O4HWy{qfz@V4o>|~PDlZpNpD8>ptlT0@iK8DBS#+mdzdPPje^4vz4xvecTJ$*QRO)k1+*#mm|oB#?RXmys9 zmby4zS32>HSQ#$T*Vk_e+Y$qVw8Fp0_Z2|CpI_=t4@paFf)5N51O7flaEWPXNN8w= zdrQs?s`|L+gCs^Ep$OfQ%3}+ESPr^2q*vS{__6`1-SXwXrgIC z$+MF3oS)mXvq;Uk|LYHtENUcyY5sV^;{<948u?;5dHKy`BO^?9yC=TKWx2T)L-|={ zkA2R?HN8VGbwq8TYo%A~OQLxfer2LogWUBJVf#irK-upW-Pe z`iVuvVU@NJE4?3Y z)eIOXK2j;P1r)yAun8YWqc%fAyfZo`@}MJ*A%m+I1laK@86v)XInTe$w!(DE4^EPh=e#tv2D?uo(~ENA}61PfTgXqnv9at z!c0L0Tp2QIX|*ylKEA8pb`ORfQ<)C^{5jtdccGwwQ=(n0JITpJQ?so~bAG%kt7xyy ztTOCqbEbLMq}6^;9`nXF4Yj?Q(bBJ9*=OGXA%@xnO^UI8IEX4F}Ael83)? zZ+##ZQu9-JD)RD$>6ZMF+)i~re~$F^O?VJsphFLLeS%PDMqRdz#L}`milYda-#uev z3+Y+2_}uuMMturU^2QjhT<0#k6jSx6@W%Ne{OXHEFXKnj92wZNmqH46A>DWakEh9S zVM$@3O=~#17tS!i6jzrA*CDg%+w8beyALDB=NAp3uITCMCn8FYhKtasm^z%@T^X+L z?0h}{|Md3d(OB<&*VQ@AGzk@@5;9AYS%audNv2E@A!FuQQHD&F%u|^o^E`Eh5JED~ zgc6xE>+7ZOe#7Vc`Ru(vdw)De=H7wF4UPcwsGdYM zxWO|>(vp*jpE}_4@Zs0<=RNvLt}y8p78mcLO&Xb^D=_N(ROV*I7f~*sSRNCTw)amh zW8;y}vo`rwhK7>(&zYW*mi9QG=#944rY37M{9*!p`Tb8aqCT0;nko@*PY^|(*9@a(HH3bFztO?b+ zpVH;`-<~{ac3sTo8i_Yo+n#iO_TUTex@0&%Vkxk;rsfS?82aCwBl_}`Rc|@J2gMf0 zU{Fwyy{!?Vo{o22u^3S&nK8c~%*)$FPL1OV8-P@Y?DHx$c3`IqVvp(h-mjlO3+Z?| z!$(zCRGrUZfr-Vzt(L!|C-YbV)y)$xboJ%B??$gItE#IvC#Pw`NV(ov0lDW->C1+n z(>e3;>3Cv!j)Sy^o{~cGF?m};;@PFQ51hyw`CN|*38~e8NN9*OTpg^~T9%TITo`rr z`lhCl-eN-f4Q*}h+uGVR>{UZE$c4e4;VF}z4g?bL7G08Vn4g_J$Su}brd3wzz}`Qp zASc(Cp@rmel5zZE{TFv1A4XxjebBhfl|Rax=nmuOXJ_FcW|(Sj@UN!#+zq+L_T9VT zl0=LW_7&W0d1qBBrg_}-P+M!OoaUwXUDPGLRNYs!y1x41Ux(u*4jb#aFQ)VJ^EVy; zY}!6ADH6uP#s;%wVJjBf${oQdDEYQW>Ld7t*(buOi$8kAa_HnzoXYg_2j-Nf zi(I>ZJ+sCp3g>S0t<2B3j0PIE+e1Q>s(O}kzZv{J?-&}=Gct;6OiM?b>^)>(^~pRc zHg;#Q>c~j$#Y>k8wffa4DCW>a=9s3%#l<-~m7-`yLA2(WR8+Wp z5!JkV3ikDr@Z$Vq(*okrIy%@g@@*hZnVsp>y~UMGUdeg#{<##qIJj(Q_)#DOU}KPITO(6?<~G-Xp(@`(r`5^oaWDG-rf{y%52tRE z6=ygdIcp+iX?eraa=+b6N8;MS*{^7a1alUcP4Wwa&W9`%HebGc*^yRGk>UFKI=1Kr z;dBlQC%^^lQKXg`-ZbQNgdFe5>#O9AlWkE1-sW7PhRn{IgN~)7bXuMGf-W+d#l+dU48~(8pwih**VUvl)}GE8o@zNn z%Hknus8$zmBL{5B0uJ;{$I9>v^^KgiiyV#BdYaPG^Cfm|Ynyd+;z~;w`wA_lhVHHJ z#Ox?wd0FDC-N@D8oSd@{9-L=jh(}>?cHRijY8{uJwnL}1l@%z@o<1E=TaGq1FK-UM z!Y-3N%ZpR}z-HVZMLI07onQL(gwWy%!RD!Kln}zXY-D6e7J*V9G|_su%&q+}aJ+Lz z5eu?#ajpEkO}-C7FWX@Eps60>a-A!ER4urBI+PUaS_^L^j zkHL)vcdp-02p78>SFWs_RjHzo+_BiFIXN!E(zPF6rwfw%_Fc$hLAQ)=xpu$$?Jbi3 ze_*)auzu+9aFD;2Kx7}mrGKO@I4)67Orl@k{2;8frKNp`hi|ah9TB*(K0l67yYY`7 zDdgk`u;J+F>UvyK*nKZGG&%V(QX}l`QE*YfQk2poczwEJnD#*sV@WLJ z91z;EoxlDTmmlqNQ&ZD}&+125iXbFmZP}2i~rg(an)BxOolk# znZ4D8s`KfKG(Q;DUQwV7J~|friShP$dHKpVWf_^M(OU)vrg+Mcy>Z9bxHCnmQQQKF z6i-5yVqU-YSyPddd-`-nQF1&wsuO#j7`~3Lb1*X>o07KHcZWw0s^Vl%VRdx*(997L z$C%jI`dc|^b5XCpJx4FIHd{CHWzvV2+u##5*%o^4*}KJ4ddKQm2MX?HZrScyU^6AM zK7}pUo5~;JqV#qpnhjp4MjzKbu3qX|Cn>`Ftus6E`SW4K%vJlHiEGjqwtwn_2qD(SMMb+rg1qm!F6jZNy~ z;>HYL#>8B}W_`O&eojsccFD7|%ticc(+Kk%J0>g)a|;)k@6Jl?&2pl8mYb_v;qA*T ze~7SsqFu8QZa|&={a@jyYG-RS&_6~Mxq?tnQ8gN2IfGdtI3{_;#@3;G2i%G#rl-zO zqT;;v;Z#FILQ`jN^4ubU{h~uIe`H(oxr-CcJ|^9H}Id)oK~Ijzla=2*Uxzfe9GE8(_m>K7awtfZje>*sg3ptd{zd6cN-9vIBJ9_=G= zn($?2WIT~cl)QTNS@4+~J_m1SSf?W5@(Nb`Uli;(XaO}~{;IQyg`KLJdI;87qaGbA zDAD1GdEMh(uV}x3n;k|n*S40@^K1IFM zbP?XR6T2%U?`6!SpK=WDynLzmin=5>^`M>VOHINXWH~*3+TIZHs$anF__Y$l=B5b? zy|y=84~?Q9wG68g4smcffQ zVPP_Pxsf)bA2(vC-M(FY;bnNktDoypy)BLXjH>Qb-4W;rq{Dt}c3H;K?ba<@azo$& zdRMjfC`-RwsOWllrT4_*#Lj|yL)I!P2x0uZJQvB+F$p(XbRl)MwNep$Wo>O$ji!w; zmn7e_mb-3}QB+*Rih6g1ZORt0>ngz|)RKK0N91qZ7%(sNHEG_#Wj;I&JsZI}GEb{Q zd@NXH_|bEik9sr@E4LQ$PJQ)bO@DHxLp!QFa$pt>3+6IN&0ujLY&)HlkpcPfDWN0j z^(v6>v9Z{5I_7PrqbYB3VP%u8UsE$*fjHis8wme2vq@f{k^s)yj0M$2brHvE1s2#mFJ@%2-DcI$5n znwp}JreSNwDlm}y@(iMSMrLdk6)oU%P(f!2V%xidIj--qwc2V=O)jTT8ocR&LODCH zy16cJs*mLuwjUW|wU+sL*KjurGxOltfg266u=v|a?Myn*p=9RewK!|?J?mUN#a%*8 zcKGMzZne6P+Tqz&;|;tzfmhSIg19Zm4p45a@;d`_4^L0eyR#ps{={*J*%;+ekLv)s z=MzkCoFfxbQZD*c7>47b@dn4&Sk2At#8FE|E-{yg*);PuIbc<(nL-5n_BBMR%FAn} zstjFO;D6~^Vlh`qJrouJ7tYMMxD)G7)1Eeo=9Ay-X*{f?F=%+rEDDIk#S4C0HcyzE z^XSmzD3cCJ1qIzd+~hEGbje9`+GX2N7g7ma&PPW!jq_uKIz*z20=lk zOP5qs9iE_F4()7#8R+&oHrFCJN2e=iS`M6Jr}D16lg-X1#mdS`L9y<l#++0S)*c5td<)Hl8@& z*C+Vw+4Z*6)wJA2A>4qWp=cCW-qYbMOib=eY!97~vPY#h z+~SS;){G4dRFstqrtPR4jkd|}EYBf3>_lWa`5q)Atu+#*0PG}8pi;*-eGlG>|FsM_VJ^lP}P*@LUPJo%>(Mn8eif=$$dhd9gU5Gpt1&O;=}1VI3~x(&)EMs2{rx}x0eYCMJAEODBP9a1YWh* zsLyET@i>tS8e2DaQ9D}i6mi*{&MFm7A2OY~!}5B2`V^tR!=1Bveac1R1OtPklg;S^ zK6;J&2tWxr@fWua?tse`P%d_}WGWLOwWaGkHe zEZ8yf^>7L3>C@5-X=%U1)mK+1uBLcD$_xE*V(Ym2{R$5^kV}bQ1$b4x#AD@_{jPeC zi(gVtPyXBxu4k-!#-nbesB*pLNLUG~x&je>$^(_GoG6lhUt-hKBqR<;Vuz?LXf_ zxI=$Vyb8i?f|a#~J3>xKDni{%cF z3WJtF(NiPO6@h3qg6mkqWMk{`tQ>8BSZFFb0xH$vGXlXq23%@Lpa zt(jdWiY(sNBh78By`iD1s%quu>gRmZ(`W<^Qd&jUR(RkOa0X;%t=v>n3a-=!lfZD| zMB2@fri`uMs@Jn8F~a~k!Ns*){J)P_LH%ASGw&0QDVx}14DRmIw`1Ng8grPAZh`Q!cd3jUSG+|t=yEkP16SC31F%RF~s9FvikOku=4P& z-lp93PVLMw0z04e>p|cy=bmqhW+6$Mdnp5IA_S~vBlxq|))w+7tXe-hy~L+h`_nPA zvqSq`5y>mG{l(ldpxmQDc%r{gOaMG7DP7bc5G9oKwhB<*r2{*ox3=$X;*P(h62q1w z<6ggE@vw*g8(92*#?Bu!dIOLf9X&bQAIKUUrY3&n$|*LsVTi{OoZcc2gExfdeO;(C z(2FA(LLbfAlf+|O?KgFpmdesx_Z-vZt5XHFVBviIx`v#}BJwx^F#7TaM?`%7ob~u>)t^SevT|~#P7Hg(=o3!A zjS>9q93#dSwXO!gp#O{}F`%P3WtkI@m85e!`^o5r!}rvza-PzApuZ3@d(oDqn+C!= zdYKd_d|#$n{SV4N+?s$G%*@P4N;?VLW=!T8hqtuc5E4FnRxP17Jw<@0vD6`D>y{Nf z<<)I%4=SIb(i9Tb`xqpH^z^>w6r_cA`nc0P3k-z3WEh}k((u)3>*XMJ3mE7bSI_PU z;HD%Jw{mFu=`ZStvV!tb&w7XQgKKRLY&@-jwBF&UQDpeS9DwxC)l#>VJ>gwuTR=$;YW{b|`clQ+GCw}n289|alc!dcW z#j7kkcV-t_e5Iy#IZ6trx2xGFfp!aX?Xf|-Yad^pHT^^*l4x~L<&5*1vW0~_KYu4R zr^aMAwWr4gT>Znt6bECkU%R$+k1XfelWz|_qM5dG;fVL}SeGLH0@|Gq@#x<=_qGq7 zsWjo67Z&`G2q+>_OhzWYJyZ{my-809I%8)asxj#(qjDYYYM)a^_j<)i*6EGt2d#W7 z4EXQFW~py+8~la@rSVr!Hg+qLg0lhNm`5!M96c)ZsbX>jU9RzFxn>-l2kH_wJ zJsKi)k1o0w=|ADZtKq=i(ln=DzHADwz1JG^>+cgi496FAy$HcQ7P}wQ%^zFWLZvWTOCF|;kd%}}4(^NOo~{d^QE{rFX~qcKY;b8W|y5F4(=oD4xQ^gk*2urF)T8sjXjd zHyIfk|N6SRMJy}T*XQQvQ*bw;nxG7Tv4(oX%L^-Q=3PA5{m#WPFOG5|rrWkx{B

aa``qkmQ&q_jhg$iJl9U0 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 literal 0 HcmV?d00001 diff --git a/docs/wiki/Getting-Started-da.md b/docs/wiki/Getting-Started-da.md new file mode 100644 index 0000000..41acdbb --- /dev/null +++ b/docs/wiki/Getting-Started-da.md @@ -0,0 +1,63 @@ +[English](Getting-Started) · **Dansk** · [Bahasa Malaysia](Getting-Started-ms) + +# Kom i gang + +## Krav + +- Python 3.13 eller nyere +- Et Sheety-projekt til din kopi af Google Sheet med arkene `prices` og `users` (Bearer-godkendelse, PUT slået til på `prices`) +- En SerpAPI-nøgle eller adgangsoplysninger til mock-flyservicen +- En e-mailkonto med en app-adgangskode til at sende tilbudsmails +- Valgfrit: `pywhatkit`, en browser og en logget ind WhatsApp Web-session + +## Opsætning + +Opret et lokalt virtuelt miljø, opgradér pip og installér projektet: + +```powershell +py -3.13 -m venv .venv +.\.venv\Scripts\Activate.ps1 +python -m pip install --upgrade pip +python -m pip install -e ".[dev]" +``` + +```bash +python3.13 -m venv .venv +source .venv/bin/activate +python -m pip install --upgrade pip +python -m pip install -e ".[dev]" +``` + +## Konfiguration + +Kopiér `.env.example` til `.env` og udfyld værdierne. Læg aldrig `.env` i git. Hold dig til 3 til 5 destinationer i arket for at holde dig inden for de gratis grænser, og prøv programmet med et test-ark `users`, der kun indeholder din egen adresse. + +| Variabel | Betydning | +| --- | --- | +| `SHEETY_PRICES_ENDPOINT` | Endpoint for arket `prices` | +| `SHEETY_USERS_ENDPOINT` | Endpoint for arket `users` (`firstName`, `lastName`, `email`) | +| `SHEETY_TOKEN` | Bearer-token til Sheety-projektet | +| `FLIGHT_PROVIDER` | `serpapi` eller `mock` | +| `SERPAPI_API_KEY` | SerpAPI-nøgle | +| `MOCK_FLIGHTS_APP_ID`, `MOCK_FLIGHTS_APP_KEY` | Adgangsoplysninger til mock-flyservicen | +| `SMTP_ADDRESS` | Din mailudbyders SMTP-server (port 587) | +| `EMAIL_ADDRESS`, `EMAIL_PASSWORD` | Afsenderadresse og dens app-adgangskode | +| `WHATSAPP_NUMBER` | Dit eget nummer med landekode | + +## Kørsel + +Start programmet. Uden SMTP-indstillinger eller WhatsApp-nummer bliver beskeden skrevet i konsollen i stedet. + +```bash +python -m flight_club +``` + +## Test + +Testene bruger intet netværk, sender ingen e-mails og læser aldrig din `.env`. + +```bash +python -m pytest +``` + +![Testkørsel](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/040-personal-flight-club/raw/branch/main/docs/images/tests-run.png) diff --git a/docs/wiki/Getting-Started-ms.md b/docs/wiki/Getting-Started-ms.md new file mode 100644 index 0000000..53dcd25 --- /dev/null +++ b/docs/wiki/Getting-Started-ms.md @@ -0,0 +1,63 @@ +[English](Getting-Started) · [Dansk](Getting-Started-da) · **Bahasa Malaysia** + +# Bermula + +## Keperluan + +- Python 3.13 atau lebih baharu +- Projek Sheety untuk salinan Google Sheet anda dengan helaian `prices` dan `users` (pengesahan Bearer, PUT dihidupkan pada `prices`) +- Kunci SerpAPI atau kelayakan perkhidmatan penerbangan mock +- Akaun e-mel dengan kata laluan aplikasi untuk menghantar e-mel tawaran +- Pilihan: `pywhatkit`, pelayar dan sesi WhatsApp Web yang telah log masuk + +## Pemasangan + +Cipta persekitaran maya setempat, naik taraf pip dan pasang projek: + +```powershell +py -3.13 -m venv .venv +.\.venv\Scripts\Activate.ps1 +python -m pip install --upgrade pip +python -m pip install -e ".[dev]" +``` + +```bash +python3.13 -m venv .venv +source .venv/bin/activate +python -m pip install --upgrade pip +python -m pip install -e ".[dev]" +``` + +## Konfigurasi + +Salin `.env.example` ke `.env` dan isi nilainya. Jangan sekali-kali commit `.env`. Kekalkan 3 hingga 5 destinasi dalam helaian supaya kekal dalam had percuma, dan cuba program dengan helaian `users` ujian yang hanya mengandungi alamat anda sendiri. + +| Pemboleh ubah | Maksud | +| --- | --- | +| `SHEETY_PRICES_ENDPOINT` | Endpoint helaian `prices` | +| `SHEETY_USERS_ENDPOINT` | Endpoint helaian `users` (`firstName`, `lastName`, `email`) | +| `SHEETY_TOKEN` | Token Bearer projek Sheety | +| `FLIGHT_PROVIDER` | `serpapi` atau `mock` | +| `SERPAPI_API_KEY` | Kunci SerpAPI | +| `MOCK_FLIGHTS_APP_ID`, `MOCK_FLIGHTS_APP_KEY` | Kelayakan perkhidmatan penerbangan mock | +| `SMTP_ADDRESS` | Pelayan SMTP pembekal e-mel anda (port 587) | +| `EMAIL_ADDRESS`, `EMAIL_PASSWORD` | Alamat penghantar dan kata laluan aplikasinya | +| `WHATSAPP_NUMBER` | Nombor anda dengan kod negara | + +## Jalankan + +Mulakan program. Tanpa tetapan SMTP atau nombor WhatsApp, mesej dicetak sebagai ganti. + +```bash +python -m flight_club +``` + +## Ujian + +Ujian tidak menggunakan rangkaian, tidak menghantar e-mel dan tidak membaca `.env` anda. + +```bash +python -m pytest +``` + +![Larian ujian](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/040-personal-flight-club/raw/branch/main/docs/images/tests-run.png) diff --git a/docs/wiki/Getting-Started.md b/docs/wiki/Getting-Started.md new file mode 100644 index 0000000..a5785f9 --- /dev/null +++ b/docs/wiki/Getting-Started.md @@ -0,0 +1,63 @@ +**English** · [Dansk](Getting-Started-da) · [Bahasa Malaysia](Getting-Started-ms) + +# Getting started + +## Requirements + +- Python 3.13 or newer +- A Sheety project for your copy of the Google Sheet, with a `prices` sheet and a `users` sheet (Bearer authentication, PUT enabled on `prices`) +- A SerpAPI key or the mock flight service credentials +- An email account with an app password, to send the deal emails +- Optional: `pywhatkit`, a browser and a logged-in WhatsApp Web session + +## Setup + +Create a local virtual environment, upgrade pip and install the project: + +```powershell +py -3.13 -m venv .venv +.\.venv\Scripts\Activate.ps1 +python -m pip install --upgrade pip +python -m pip install -e ".[dev]" +``` + +```bash +python3.13 -m venv .venv +source .venv/bin/activate +python -m pip install --upgrade pip +python -m pip install -e ".[dev]" +``` + +## Configuration + +Copy `.env.example` to `.env` and fill in the values. Never commit `.env`. Keep 3 to 5 destinations in the sheet to stay inside the free tiers, and try the program with a test `users` sheet that holds only your own address. + +| Variable | Meaning | +| --- | --- | +| `SHEETY_PRICES_ENDPOINT` | Endpoint of the `prices` sheet | +| `SHEETY_USERS_ENDPOINT` | Endpoint of the `users` sheet (`firstName`, `lastName`, `email`) | +| `SHEETY_TOKEN` | Bearer token of the Sheety project | +| `FLIGHT_PROVIDER` | `serpapi` or `mock` | +| `SERPAPI_API_KEY` | SerpAPI key | +| `MOCK_FLIGHTS_APP_ID`, `MOCK_FLIGHTS_APP_KEY` | Mock flight service credentials | +| `SMTP_ADDRESS` | SMTP server of your mail provider (port 587) | +| `EMAIL_ADDRESS`, `EMAIL_PASSWORD` | Sender address and its app password | +| `WHATSAPP_NUMBER` | Your own number with country code | + +## Run + +Start the program. Without the SMTP settings or a WhatsApp number the message is printed instead. + +```bash +python -m flight_club +``` + +## Tests + +The tests use no network, send no email and never read your `.env`. + +```bash +python -m pytest +``` + +![Test run](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/040-personal-flight-club/raw/branch/main/docs/images/tests-run.png) diff --git a/docs/wiki/Home-da.md b/docs/wiki/Home-da.md new file mode 100644 index 0000000..d03d752 --- /dev/null +++ b/docs/wiki/Home-da.md @@ -0,0 +1,18 @@ +[English](Home) · **Dansk** · [Bahasa Malaysia](Home-ms) + +# Personal Flight Club + +En flyklub fra dag 40 i Udemys 100 Days of Code. Programmet læser destinationer og prisgrænser fra et Google Sheet, finder den billigste returflyvning de næste seks måneder (først direkte, derefter med mellemlandinger) og sender en e-mail til alle kunder i et andet ark, når en pris er under grænsen. + +## Sådan virker det + +1. Læs rækkerne (by, IATA-kode, laveste pris) fra arket `prices` via Sheety. +2. Søg efter den billigste direkte returflyvning fra i morgen til seks måneder frem for hver række, med SerpAPI Google Flights eller kursets mock-flyservice. +3. Hvis der ikke er en direkte flyvning, søges der igen efter flyvninger med op til to stop, og antallet af stop noteres. +4. Hvis prisen er lavere end arkets laveste pris, opdateres arket, der sendes en WhatsApp-besked, og alle kunder i arket `users` får en e-mail. En flyvning med stop siger "Flight has 1 stopover." i beskeden. + +## Et eksempel på en kørsel med mock-data + +![Et eksempel på en kørsel med mock-data](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/040-personal-flight-club/raw/branch/main/docs/images/sample-run.png) + +Næste: [Kom i gang](Getting-Started-da) diff --git a/docs/wiki/Home-ms.md b/docs/wiki/Home-ms.md new file mode 100644 index 0000000..3396be2 --- /dev/null +++ b/docs/wiki/Home-ms.md @@ -0,0 +1,18 @@ +[English](Home) · [Dansk](Home-da) · **Bahasa Malaysia** + +# Personal Flight Club + +Kelab penerbangan dari hari ke-40 kursus Udemy 100 Days of Code. Program ini membaca destinasi dan had harga daripada Google Sheet, mencari penerbangan pergi-balik termurah untuk enam bulan akan datang (penerbangan terus dahulu, kemudian dengan persinggahan) dan menghantar e-mel kepada setiap pelanggan dalam helaian kedua apabila harga lebih rendah daripada had. + +## Cara ia berfungsi + +1. Baca baris (bandar, kod IATA, harga terendah) daripada helaian `prices` melalui Sheety. +2. Cari penerbangan pergi-balik terus yang termurah dari esok hingga enam bulan lagi untuk setiap baris, dengan SerpAPI Google Flights atau perkhidmatan penerbangan olok-olok (mock) kursus. +3. Jika tiada penerbangan terus, cari semula penerbangan dengan sehingga dua persinggahan dan catat bilangan persinggahan. +4. Jika harga lebih rendah daripada harga terendah dalam helaian, kemas kini helaian, hantar mesej WhatsApp dan e-mel kepada setiap pelanggan dalam helaian `users`. Penerbangan dengan persinggahan menyatakan "Flight has 1 stopover." dalam mesej. + +## Contoh larian dengan data mock + +![Contoh larian dengan data mock](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/040-personal-flight-club/raw/branch/main/docs/images/sample-run.png) + +Seterusnya: [Bermula](Getting-Started-ms) diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md new file mode 100644 index 0000000..0cc0bb6 --- /dev/null +++ b/docs/wiki/Home.md @@ -0,0 +1,18 @@ +**English** · [Dansk](Home-da) · [Bahasa Malaysia](Home-ms) + +# Personal Flight Club + +A flight club from Day 40 of Udemy's 100 Days of Code. It reads destinations and price limits from a Google Sheet, finds the cheapest return flight for the next six months (direct first, then with stopovers) and emails every customer in a second sheet when a price is below the limit. + +## How it works + +1. Read the rows (city, IATA code, lowest price) from the `prices` sheet through Sheety. +2. Search the cheapest direct return flight from tomorrow to six months ahead for every row, with SerpAPI Google Flights or the course's mock flight service. +3. If there is no direct flight, search again for flights with up to two stops and note the number of stops. +4. If the price is lower than the sheet's lowest price, update the sheet, send a WhatsApp message and email every customer in the `users` sheet. A flight with stops says "Flight has 1 stopover." in the message. + +## A sample run with mock data + +![A sample run with mock data](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/040-personal-flight-club/raw/branch/main/docs/images/sample-run.png) + +Next: [Getting started](Getting-Started) diff --git a/docs/wiki/_Sidebar.md b/docs/wiki/_Sidebar.md new file mode 100644 index 0000000..7bff9a2 --- /dev/null +++ b/docs/wiki/_Sidebar.md @@ -0,0 +1,14 @@ +**English** + +- [Home](Home) +- [Getting started](Getting-Started) + +**Dansk** + +- [Forside](Home-da) +- [Kom i gang](Getting-Started-da) + +**Bahasa Malaysia** + +- [Laman utama](Home-ms) +- [Bermula](Getting-Started-ms) -- 2.54.0