From 86657cddf22c951c10628ebf6e1b057a4926690b Mon Sep 17 00:00:00 2001 From: ci-bot Date: Fri, 5 Jan 2024 07:43:28 +0000 Subject: [PATCH] Removed pr-24 with mike 1.1.2 --- pr-24/404.html | 841 --- pr-24/assets/_mkdocstrings.css | 36 - pr-24/assets/images/favicon.png | Bin 1870 -> 0 bytes .../assets/javascripts/bundle.fac441b0.min.js | 29 - .../javascripts/bundle.fac441b0.min.js.map | 8 - .../javascripts/lunr/min/lunr.ar.min.js | 1 - .../javascripts/lunr/min/lunr.da.min.js | 18 - .../javascripts/lunr/min/lunr.de.min.js | 18 - .../javascripts/lunr/min/lunr.du.min.js | 18 - .../javascripts/lunr/min/lunr.es.min.js | 18 - .../javascripts/lunr/min/lunr.fi.min.js | 18 - .../javascripts/lunr/min/lunr.fr.min.js | 18 - .../javascripts/lunr/min/lunr.hi.min.js | 1 - .../javascripts/lunr/min/lunr.hu.min.js | 18 - .../javascripts/lunr/min/lunr.hy.min.js | 1 - .../javascripts/lunr/min/lunr.it.min.js | 18 - .../javascripts/lunr/min/lunr.ja.min.js | 1 - .../javascripts/lunr/min/lunr.jp.min.js | 1 - .../javascripts/lunr/min/lunr.kn.min.js | 1 - .../javascripts/lunr/min/lunr.ko.min.js | 1 - .../javascripts/lunr/min/lunr.multi.min.js | 1 - .../javascripts/lunr/min/lunr.nl.min.js | 18 - .../javascripts/lunr/min/lunr.no.min.js | 18 - .../javascripts/lunr/min/lunr.pt.min.js | 18 - .../javascripts/lunr/min/lunr.ro.min.js | 18 - .../javascripts/lunr/min/lunr.ru.min.js | 18 - .../javascripts/lunr/min/lunr.sa.min.js | 1 - .../lunr/min/lunr.stemmer.support.min.js | 1 - .../javascripts/lunr/min/lunr.sv.min.js | 18 - .../javascripts/lunr/min/lunr.ta.min.js | 1 - .../javascripts/lunr/min/lunr.te.min.js | 1 - .../javascripts/lunr/min/lunr.th.min.js | 1 - .../javascripts/lunr/min/lunr.tr.min.js | 18 - .../javascripts/lunr/min/lunr.vi.min.js | 1 - .../javascripts/lunr/min/lunr.zh.min.js | 1 - pr-24/assets/javascripts/lunr/tinyseg.js | 206 - pr-24/assets/javascripts/lunr/wordcut.js | 6708 ----------------- .../workers/search.208ed371.min.js | 42 - .../workers/search.208ed371.min.js.map | 8 - .../assets/stylesheets/main.85bb2934.min.css | 1 - .../stylesheets/main.85bb2934.min.css.map | 1 - .../stylesheets/palette.a6bdf11c.min.css | 1 - .../stylesheets/palette.a6bdf11c.min.css.map | 1 - pr-24/coverage/.gitignore | 2 - pr-24/coverage/coverage_html.js | 624 -- pr-24/coverage/covindex.html | 146 - .../d_ebaf54d0d3802af7___init___py.html | 109 - .../d_ebaf54d0d3802af7_commands_py.html | 379 - .../d_ebaf54d0d3802af7_configure_py.html | 196 - .../d_ebaf54d0d3802af7_inputformat_py.html | 390 - .../coverage/d_ebaf54d0d3802af7_rwxlb_py.html | 496 -- pr-24/coverage/favicon_32.png | Bin 1732 -> 0 bytes pr-24/coverage/index.html | 946 --- pr-24/coverage/keybd_closed.png | Bin 9004 -> 0 bytes pr-24/coverage/keybd_open.png | Bin 9003 -> 0 bytes pr-24/coverage/status.json | 1 - pr-24/coverage/style.css | 309 - pr-24/developer_guide/contributing/index.html | 1204 --- pr-24/developer_guide/docs/index.html | 1095 --- .../developer_guide/github_actions/index.html | 1259 ---- pr-24/developer_guide/releases/index.html | 992 --- pr-24/developer_guide/vscode/index.html | 1180 --- pr-24/gen_ref_pages.py | 32 - pr-24/index.html | 920 --- pr-24/javascripts/mathjax.js | 16 - pr-24/license/index.html | 948 --- pr-24/objects.inv | Bin 482 -> 0 bytes pr-24/overrides/main.html | 8 - pr-24/reference/SUMMARY/index.html | 859 --- pr-24/reference/xlbudget/commands/index.html | 2776 ------- pr-24/reference/xlbudget/configure/index.html | 1420 ---- pr-24/reference/xlbudget/index.html | 1007 --- .../reference/xlbudget/inputformat/index.html | 1963 ----- pr-24/reference/xlbudget/rwxlb/index.html | 2055 ----- pr-24/search/search_index.json | 1 - pr-24/sitemap.xml | 88 - pr-24/sitemap.xml.gz | Bin 369 -> 0 bytes pr-24/user_guide/commands/index.html | 939 --- pr-24/user_guide/configuration/index.html | 939 --- pr-24/user_guide/getting_started/index.html | 939 --- pr-24/user_guide/installation/index.html | 939 --- versions.json | 2 +- 82 files changed, 1 insertion(+), 33345 deletions(-) delete mode 100644 pr-24/404.html delete mode 100644 pr-24/assets/_mkdocstrings.css delete mode 100644 pr-24/assets/images/favicon.png delete mode 100644 pr-24/assets/javascripts/bundle.fac441b0.min.js delete mode 100644 pr-24/assets/javascripts/bundle.fac441b0.min.js.map delete mode 100644 pr-24/assets/javascripts/lunr/min/lunr.ar.min.js delete mode 100644 pr-24/assets/javascripts/lunr/min/lunr.da.min.js delete mode 100644 pr-24/assets/javascripts/lunr/min/lunr.de.min.js delete mode 100644 pr-24/assets/javascripts/lunr/min/lunr.du.min.js delete mode 100644 pr-24/assets/javascripts/lunr/min/lunr.es.min.js delete mode 100644 pr-24/assets/javascripts/lunr/min/lunr.fi.min.js delete mode 100644 pr-24/assets/javascripts/lunr/min/lunr.fr.min.js delete mode 100644 pr-24/assets/javascripts/lunr/min/lunr.hi.min.js delete mode 100644 pr-24/assets/javascripts/lunr/min/lunr.hu.min.js delete mode 100644 pr-24/assets/javascripts/lunr/min/lunr.hy.min.js delete mode 100644 pr-24/assets/javascripts/lunr/min/lunr.it.min.js delete mode 100644 pr-24/assets/javascripts/lunr/min/lunr.ja.min.js delete mode 100644 pr-24/assets/javascripts/lunr/min/lunr.jp.min.js delete mode 100644 pr-24/assets/javascripts/lunr/min/lunr.kn.min.js delete mode 100644 pr-24/assets/javascripts/lunr/min/lunr.ko.min.js delete mode 100644 pr-24/assets/javascripts/lunr/min/lunr.multi.min.js delete mode 100644 pr-24/assets/javascripts/lunr/min/lunr.nl.min.js delete mode 100644 pr-24/assets/javascripts/lunr/min/lunr.no.min.js delete mode 100644 pr-24/assets/javascripts/lunr/min/lunr.pt.min.js delete mode 100644 pr-24/assets/javascripts/lunr/min/lunr.ro.min.js delete mode 100644 pr-24/assets/javascripts/lunr/min/lunr.ru.min.js delete mode 100644 pr-24/assets/javascripts/lunr/min/lunr.sa.min.js delete mode 100644 pr-24/assets/javascripts/lunr/min/lunr.stemmer.support.min.js delete mode 100644 pr-24/assets/javascripts/lunr/min/lunr.sv.min.js delete mode 100644 pr-24/assets/javascripts/lunr/min/lunr.ta.min.js delete mode 100644 pr-24/assets/javascripts/lunr/min/lunr.te.min.js delete mode 100644 pr-24/assets/javascripts/lunr/min/lunr.th.min.js delete mode 100644 pr-24/assets/javascripts/lunr/min/lunr.tr.min.js delete mode 100644 pr-24/assets/javascripts/lunr/min/lunr.vi.min.js delete mode 100644 pr-24/assets/javascripts/lunr/min/lunr.zh.min.js delete mode 100644 pr-24/assets/javascripts/lunr/tinyseg.js delete mode 100644 pr-24/assets/javascripts/lunr/wordcut.js delete mode 100644 pr-24/assets/javascripts/workers/search.208ed371.min.js delete mode 100644 pr-24/assets/javascripts/workers/search.208ed371.min.js.map delete mode 100644 pr-24/assets/stylesheets/main.85bb2934.min.css delete mode 100644 pr-24/assets/stylesheets/main.85bb2934.min.css.map delete mode 100644 pr-24/assets/stylesheets/palette.a6bdf11c.min.css delete mode 100644 pr-24/assets/stylesheets/palette.a6bdf11c.min.css.map delete mode 100644 pr-24/coverage/.gitignore delete mode 100644 pr-24/coverage/coverage_html.js delete mode 100644 pr-24/coverage/covindex.html delete mode 100644 pr-24/coverage/d_ebaf54d0d3802af7___init___py.html delete mode 100644 pr-24/coverage/d_ebaf54d0d3802af7_commands_py.html delete mode 100644 pr-24/coverage/d_ebaf54d0d3802af7_configure_py.html delete mode 100644 pr-24/coverage/d_ebaf54d0d3802af7_inputformat_py.html delete mode 100644 pr-24/coverage/d_ebaf54d0d3802af7_rwxlb_py.html delete mode 100644 pr-24/coverage/favicon_32.png delete mode 100644 pr-24/coverage/index.html delete mode 100644 pr-24/coverage/keybd_closed.png delete mode 100644 pr-24/coverage/keybd_open.png delete mode 100644 pr-24/coverage/status.json delete mode 100644 pr-24/coverage/style.css delete mode 100644 pr-24/developer_guide/contributing/index.html delete mode 100644 pr-24/developer_guide/docs/index.html delete mode 100644 pr-24/developer_guide/github_actions/index.html delete mode 100644 pr-24/developer_guide/releases/index.html delete mode 100644 pr-24/developer_guide/vscode/index.html delete mode 100644 pr-24/gen_ref_pages.py delete mode 100644 pr-24/index.html delete mode 100644 pr-24/javascripts/mathjax.js delete mode 100644 pr-24/license/index.html delete mode 100644 pr-24/objects.inv delete mode 100644 pr-24/overrides/main.html delete mode 100644 pr-24/reference/SUMMARY/index.html delete mode 100644 pr-24/reference/xlbudget/commands/index.html delete mode 100644 pr-24/reference/xlbudget/configure/index.html delete mode 100644 pr-24/reference/xlbudget/index.html delete mode 100644 pr-24/reference/xlbudget/inputformat/index.html delete mode 100644 pr-24/reference/xlbudget/rwxlb/index.html delete mode 100644 pr-24/search/search_index.json delete mode 100644 pr-24/sitemap.xml delete mode 100644 pr-24/sitemap.xml.gz delete mode 100644 pr-24/user_guide/commands/index.html delete mode 100644 pr-24/user_guide/configuration/index.html delete mode 100644 pr-24/user_guide/getting_started/index.html delete mode 100644 pr-24/user_guide/installation/index.html diff --git a/pr-24/404.html b/pr-24/404.html deleted file mode 100644 index 4f51410..0000000 --- a/pr-24/404.html +++ /dev/null @@ -1,841 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - Xlbudget Docs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
- -
- - - - - - - - -
- - - - - - - -
- -
- - - - -
-
- - - -
-
-
- - - - - - -
-
-
- - - -
-
-
- - - -
-
-
- - - -
-
- -

404 - Not found

- -
-
- - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pr-24/assets/_mkdocstrings.css b/pr-24/assets/_mkdocstrings.css deleted file mode 100644 index a65078d..0000000 --- a/pr-24/assets/_mkdocstrings.css +++ /dev/null @@ -1,36 +0,0 @@ - -/* Don't capitalize names. */ -h5.doc-heading { - text-transform: none !important; -} - -/* Avoid breaking parameters name, etc. in table cells. */ -.doc-contents td code { - word-break: normal !important; -} - -/* For pieces of Markdown rendered in table cells. */ -.doc-contents td p { - margin-top: 0 !important; - margin-bottom: 0 !important; -} - -/* Max width for docstring sections tables. */ -.doc .md-typeset__table, -.doc .md-typeset__table table { - display: table !important; - width: 100%; -} -.doc .md-typeset__table tr { - display: table-row; -} - -/* Avoid line breaks in rendered fields. */ -.field-body p { - display: inline; -} - -/* Defaults in Spacy table style. */ -.doc-param-default { - float: right; -} diff --git a/pr-24/assets/images/favicon.png b/pr-24/assets/images/favicon.png deleted file mode 100644 index 1cf13b9f9d978896599290a74f77d5dbe7d1655c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1870 zcmV-U2eJ5xP)Gc)JR9QMau)O=X#!i9;T z37kk-upj^(fsR36MHs_+1RCI)NNu9}lD0S{B^g8PN?Ww(5|~L#Ng*g{WsqleV}|#l zz8@ri&cTzw_h33bHI+12+kK6WN$h#n5cD8OQt`5kw6p~9H3()bUQ8OS4Q4HTQ=1Ol z_JAocz`fLbT2^{`8n~UAo=#AUOf=SOq4pYkt;XbC&f#7lb$*7=$na!mWCQ`dBQsO0 zLFBSPj*N?#u5&pf2t4XjEGH|=pPQ8xh7tpx;US5Cx_Ju;!O`ya-yF`)b%TEt5>eP1ZX~}sjjA%FJF?h7cX8=b!DZl<6%Cv z*G0uvvU+vmnpLZ2paivG-(cd*y3$hCIcsZcYOGh{$&)A6*XX&kXZd3G8m)G$Zz-LV z^GF3VAW^Mdv!)4OM8EgqRiz~*Cji;uzl2uC9^=8I84vNp;ltJ|q-*uQwGp2ma6cY7 z;`%`!9UXO@fr&Ebapfs34OmS9^u6$)bJxrucutf>`dKPKT%%*d3XlFVKunp9 zasduxjrjs>f8V=D|J=XNZp;_Zy^WgQ$9WDjgY=z@stwiEBm9u5*|34&1Na8BMjjgf3+SHcr`5~>oz1Y?SW^=K z^bTyO6>Gar#P_W2gEMwq)ot3; zREHn~U&Dp0l6YT0&k-wLwYjb?5zGK`W6S2v+K>AM(95m2C20L|3m~rN8dprPr@t)5lsk9Hu*W z?pS990s;Ez=+Rj{x7p``4>+c0G5^pYnB1^!TL=(?HLHZ+HicG{~4F1d^5Awl_2!1jICM-!9eoLhbbT^;yHcefyTAaqRcY zmuctDopPT!%k+}x%lZRKnzykr2}}XfG_ne?nRQO~?%hkzo;@RN{P6o`&mMUWBYMTe z6i8ChtjX&gXl`nvrU>jah)2iNM%JdjqoaeaU%yVn!^70x-flljp6Q5tK}5}&X8&&G zX3fpb3E(!rH=zVI_9Gjl45w@{(ITqngWFe7@9{mX;tO25Z_8 zQHEpI+FkTU#4xu>RkN>b3Tnc3UpWzPXWm#o55GKF09j^Mh~)K7{QqbO_~(@CVq! zS<8954|P8mXN2MRs86xZ&Q4EfM@JB94b=(YGuk)s&^jiSF=t3*oNK3`rD{H`yQ?d; ztE=laAUoZx5?RC8*WKOj`%LXEkgDd>&^Q4M^z`%u0rg-It=hLCVsq!Z%^6eB-OvOT zFZ28TN&cRmgU}Elrnk43)!>Z1FCPL2K$7}gwzIc48NX}#!A1BpJP?#v5wkNprhV** z?Cpalt1oH&{r!o3eSKc&ap)iz2BTn_VV`4>9M^b3;(YY}4>#ML6{~(4mH+?%07*qo IM6N<$f(jP3KmY&$ diff --git a/pr-24/assets/javascripts/bundle.fac441b0.min.js b/pr-24/assets/javascripts/bundle.fac441b0.min.js deleted file mode 100644 index 4bb4cd6..0000000 --- a/pr-24/assets/javascripts/bundle.fac441b0.min.js +++ /dev/null @@ -1,29 +0,0 @@ -"use strict";(()=>{var Ci=Object.create;var gr=Object.defineProperty;var Ri=Object.getOwnPropertyDescriptor;var ki=Object.getOwnPropertyNames,Ht=Object.getOwnPropertySymbols,Hi=Object.getPrototypeOf,yr=Object.prototype.hasOwnProperty,nn=Object.prototype.propertyIsEnumerable;var rn=(e,t,r)=>t in e?gr(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,P=(e,t)=>{for(var r in t||(t={}))yr.call(t,r)&&rn(e,r,t[r]);if(Ht)for(var r of Ht(t))nn.call(t,r)&&rn(e,r,t[r]);return e};var on=(e,t)=>{var r={};for(var n in e)yr.call(e,n)&&t.indexOf(n)<0&&(r[n]=e[n]);if(e!=null&&Ht)for(var n of Ht(e))t.indexOf(n)<0&&nn.call(e,n)&&(r[n]=e[n]);return r};var Pt=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Pi=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of ki(t))!yr.call(e,o)&&o!==r&&gr(e,o,{get:()=>t[o],enumerable:!(n=Ri(t,o))||n.enumerable});return e};var yt=(e,t,r)=>(r=e!=null?Ci(Hi(e)):{},Pi(t||!e||!e.__esModule?gr(r,"default",{value:e,enumerable:!0}):r,e));var sn=Pt((xr,an)=>{(function(e,t){typeof xr=="object"&&typeof an!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(xr,function(){"use strict";function e(r){var n=!0,o=!1,i=null,s={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function a(O){return!!(O&&O!==document&&O.nodeName!=="HTML"&&O.nodeName!=="BODY"&&"classList"in O&&"contains"in O.classList)}function f(O){var Qe=O.type,De=O.tagName;return!!(De==="INPUT"&&s[Qe]&&!O.readOnly||De==="TEXTAREA"&&!O.readOnly||O.isContentEditable)}function c(O){O.classList.contains("focus-visible")||(O.classList.add("focus-visible"),O.setAttribute("data-focus-visible-added",""))}function u(O){O.hasAttribute("data-focus-visible-added")&&(O.classList.remove("focus-visible"),O.removeAttribute("data-focus-visible-added"))}function p(O){O.metaKey||O.altKey||O.ctrlKey||(a(r.activeElement)&&c(r.activeElement),n=!0)}function m(O){n=!1}function d(O){a(O.target)&&(n||f(O.target))&&c(O.target)}function h(O){a(O.target)&&(O.target.classList.contains("focus-visible")||O.target.hasAttribute("data-focus-visible-added"))&&(o=!0,window.clearTimeout(i),i=window.setTimeout(function(){o=!1},100),u(O.target))}function v(O){document.visibilityState==="hidden"&&(o&&(n=!0),Q())}function Q(){document.addEventListener("mousemove",N),document.addEventListener("mousedown",N),document.addEventListener("mouseup",N),document.addEventListener("pointermove",N),document.addEventListener("pointerdown",N),document.addEventListener("pointerup",N),document.addEventListener("touchmove",N),document.addEventListener("touchstart",N),document.addEventListener("touchend",N)}function B(){document.removeEventListener("mousemove",N),document.removeEventListener("mousedown",N),document.removeEventListener("mouseup",N),document.removeEventListener("pointermove",N),document.removeEventListener("pointerdown",N),document.removeEventListener("pointerup",N),document.removeEventListener("touchmove",N),document.removeEventListener("touchstart",N),document.removeEventListener("touchend",N)}function N(O){O.target.nodeName&&O.target.nodeName.toLowerCase()==="html"||(n=!1,B())}document.addEventListener("keydown",p,!0),document.addEventListener("mousedown",m,!0),document.addEventListener("pointerdown",m,!0),document.addEventListener("touchstart",m,!0),document.addEventListener("visibilitychange",v,!0),Q(),r.addEventListener("focus",d,!0),r.addEventListener("blur",h,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)})});var cn=Pt(Er=>{(function(e){var t=function(){try{return!!Symbol.iterator}catch(c){return!1}},r=t(),n=function(c){var u={next:function(){var p=c.shift();return{done:p===void 0,value:p}}};return r&&(u[Symbol.iterator]=function(){return u}),u},o=function(c){return encodeURIComponent(c).replace(/%20/g,"+")},i=function(c){return decodeURIComponent(String(c).replace(/\+/g," "))},s=function(){var c=function(p){Object.defineProperty(this,"_entries",{writable:!0,value:{}});var m=typeof p;if(m!=="undefined")if(m==="string")p!==""&&this._fromString(p);else if(p instanceof c){var d=this;p.forEach(function(B,N){d.append(N,B)})}else if(p!==null&&m==="object")if(Object.prototype.toString.call(p)==="[object Array]")for(var h=0;hd[0]?1:0}),c._entries&&(c._entries={});for(var p=0;p1?i(d[1]):"")}})})(typeof global!="undefined"?global:typeof window!="undefined"?window:typeof self!="undefined"?self:Er);(function(e){var t=function(){try{var o=new e.URL("b","http://a");return o.pathname="c d",o.href==="http://a/c%20d"&&o.searchParams}catch(i){return!1}},r=function(){var o=e.URL,i=function(f,c){typeof f!="string"&&(f=String(f)),c&&typeof c!="string"&&(c=String(c));var u=document,p;if(c&&(e.location===void 0||c!==e.location.href)){c=c.toLowerCase(),u=document.implementation.createHTMLDocument(""),p=u.createElement("base"),p.href=c,u.head.appendChild(p);try{if(p.href.indexOf(c)!==0)throw new Error(p.href)}catch(O){throw new Error("URL unable to set base "+c+" due to "+O)}}var m=u.createElement("a");m.href=f,p&&(u.body.appendChild(m),m.href=m.href);var d=u.createElement("input");if(d.type="url",d.value=f,m.protocol===":"||!/:/.test(m.href)||!d.checkValidity()&&!c)throw new TypeError("Invalid URL");Object.defineProperty(this,"_anchorElement",{value:m});var h=new e.URLSearchParams(this.search),v=!0,Q=!0,B=this;["append","delete","set"].forEach(function(O){var Qe=h[O];h[O]=function(){Qe.apply(h,arguments),v&&(Q=!1,B.search=h.toString(),Q=!0)}}),Object.defineProperty(this,"searchParams",{value:h,enumerable:!0});var N=void 0;Object.defineProperty(this,"_updateSearchParams",{enumerable:!1,configurable:!1,writable:!1,value:function(){this.search!==N&&(N=this.search,Q&&(v=!1,this.searchParams._fromString(this.search),v=!0))}})},s=i.prototype,a=function(f){Object.defineProperty(s,f,{get:function(){return this._anchorElement[f]},set:function(c){this._anchorElement[f]=c},enumerable:!0})};["hash","host","hostname","port","protocol"].forEach(function(f){a(f)}),Object.defineProperty(s,"search",{get:function(){return this._anchorElement.search},set:function(f){this._anchorElement.search=f,this._updateSearchParams()},enumerable:!0}),Object.defineProperties(s,{toString:{get:function(){var f=this;return function(){return f.href}}},href:{get:function(){return this._anchorElement.href.replace(/\?$/,"")},set:function(f){this._anchorElement.href=f,this._updateSearchParams()},enumerable:!0},pathname:{get:function(){return this._anchorElement.pathname.replace(/(^\/?)/,"/")},set:function(f){this._anchorElement.pathname=f},enumerable:!0},origin:{get:function(){var f={"http:":80,"https:":443,"ftp:":21}[this._anchorElement.protocol],c=this._anchorElement.port!=f&&this._anchorElement.port!=="";return this._anchorElement.protocol+"//"+this._anchorElement.hostname+(c?":"+this._anchorElement.port:"")},enumerable:!0},password:{get:function(){return""},set:function(f){},enumerable:!0},username:{get:function(){return""},set:function(f){},enumerable:!0}}),i.createObjectURL=function(f){return o.createObjectURL.apply(o,arguments)},i.revokeObjectURL=function(f){return o.revokeObjectURL.apply(o,arguments)},e.URL=i};if(t()||r(),e.location!==void 0&&!("origin"in e.location)){var n=function(){return e.location.protocol+"//"+e.location.hostname+(e.location.port?":"+e.location.port:"")};try{Object.defineProperty(e.location,"origin",{get:n,enumerable:!0})}catch(o){setInterval(function(){e.location.origin=n()},100)}}})(typeof global!="undefined"?global:typeof window!="undefined"?window:typeof self!="undefined"?self:Er)});var qr=Pt((Mt,Nr)=>{/*! - * clipboard.js v2.0.11 - * https://clipboardjs.com/ - * - * Licensed MIT © Zeno Rocha - */(function(t,r){typeof Mt=="object"&&typeof Nr=="object"?Nr.exports=r():typeof define=="function"&&define.amd?define([],r):typeof Mt=="object"?Mt.ClipboardJS=r():t.ClipboardJS=r()})(Mt,function(){return function(){var e={686:function(n,o,i){"use strict";i.d(o,{default:function(){return Ai}});var s=i(279),a=i.n(s),f=i(370),c=i.n(f),u=i(817),p=i.n(u);function m(j){try{return document.execCommand(j)}catch(T){return!1}}var d=function(T){var E=p()(T);return m("cut"),E},h=d;function v(j){var T=document.documentElement.getAttribute("dir")==="rtl",E=document.createElement("textarea");E.style.fontSize="12pt",E.style.border="0",E.style.padding="0",E.style.margin="0",E.style.position="absolute",E.style[T?"right":"left"]="-9999px";var H=window.pageYOffset||document.documentElement.scrollTop;return E.style.top="".concat(H,"px"),E.setAttribute("readonly",""),E.value=j,E}var Q=function(T,E){var H=v(T);E.container.appendChild(H);var I=p()(H);return m("copy"),H.remove(),I},B=function(T){var E=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},H="";return typeof T=="string"?H=Q(T,E):T instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(T==null?void 0:T.type)?H=Q(T.value,E):(H=p()(T),m("copy")),H},N=B;function O(j){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?O=function(E){return typeof E}:O=function(E){return E&&typeof Symbol=="function"&&E.constructor===Symbol&&E!==Symbol.prototype?"symbol":typeof E},O(j)}var Qe=function(){var T=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},E=T.action,H=E===void 0?"copy":E,I=T.container,q=T.target,Me=T.text;if(H!=="copy"&&H!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(q!==void 0)if(q&&O(q)==="object"&&q.nodeType===1){if(H==="copy"&&q.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(H==="cut"&&(q.hasAttribute("readonly")||q.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(Me)return N(Me,{container:I});if(q)return H==="cut"?h(q):N(q,{container:I})},De=Qe;function $e(j){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?$e=function(E){return typeof E}:$e=function(E){return E&&typeof Symbol=="function"&&E.constructor===Symbol&&E!==Symbol.prototype?"symbol":typeof E},$e(j)}function Ei(j,T){if(!(j instanceof T))throw new TypeError("Cannot call a class as a function")}function tn(j,T){for(var E=0;E0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof I.action=="function"?I.action:this.defaultAction,this.target=typeof I.target=="function"?I.target:this.defaultTarget,this.text=typeof I.text=="function"?I.text:this.defaultText,this.container=$e(I.container)==="object"?I.container:document.body}},{key:"listenClick",value:function(I){var q=this;this.listener=c()(I,"click",function(Me){return q.onClick(Me)})}},{key:"onClick",value:function(I){var q=I.delegateTarget||I.currentTarget,Me=this.action(q)||"copy",kt=De({action:Me,container:this.container,target:this.target(q),text:this.text(q)});this.emit(kt?"success":"error",{action:Me,text:kt,trigger:q,clearSelection:function(){q&&q.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(I){return vr("action",I)}},{key:"defaultTarget",value:function(I){var q=vr("target",I);if(q)return document.querySelector(q)}},{key:"defaultText",value:function(I){return vr("text",I)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(I){var q=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return N(I,q)}},{key:"cut",value:function(I){return h(I)}},{key:"isSupported",value:function(){var I=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],q=typeof I=="string"?[I]:I,Me=!!document.queryCommandSupported;return q.forEach(function(kt){Me=Me&&!!document.queryCommandSupported(kt)}),Me}}]),E}(a()),Ai=Li},828:function(n){var o=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function s(a,f){for(;a&&a.nodeType!==o;){if(typeof a.matches=="function"&&a.matches(f))return a;a=a.parentNode}}n.exports=s},438:function(n,o,i){var s=i(828);function a(u,p,m,d,h){var v=c.apply(this,arguments);return u.addEventListener(m,v,h),{destroy:function(){u.removeEventListener(m,v,h)}}}function f(u,p,m,d,h){return typeof u.addEventListener=="function"?a.apply(null,arguments):typeof m=="function"?a.bind(null,document).apply(null,arguments):(typeof u=="string"&&(u=document.querySelectorAll(u)),Array.prototype.map.call(u,function(v){return a(v,p,m,d,h)}))}function c(u,p,m,d){return function(h){h.delegateTarget=s(h.target,p),h.delegateTarget&&d.call(u,h)}}n.exports=f},879:function(n,o){o.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},o.nodeList=function(i){var s=Object.prototype.toString.call(i);return i!==void 0&&(s==="[object NodeList]"||s==="[object HTMLCollection]")&&"length"in i&&(i.length===0||o.node(i[0]))},o.string=function(i){return typeof i=="string"||i instanceof String},o.fn=function(i){var s=Object.prototype.toString.call(i);return s==="[object Function]"}},370:function(n,o,i){var s=i(879),a=i(438);function f(m,d,h){if(!m&&!d&&!h)throw new Error("Missing required arguments");if(!s.string(d))throw new TypeError("Second argument must be a String");if(!s.fn(h))throw new TypeError("Third argument must be a Function");if(s.node(m))return c(m,d,h);if(s.nodeList(m))return u(m,d,h);if(s.string(m))return p(m,d,h);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function c(m,d,h){return m.addEventListener(d,h),{destroy:function(){m.removeEventListener(d,h)}}}function u(m,d,h){return Array.prototype.forEach.call(m,function(v){v.addEventListener(d,h)}),{destroy:function(){Array.prototype.forEach.call(m,function(v){v.removeEventListener(d,h)})}}}function p(m,d,h){return a(document.body,m,d,h)}n.exports=f},817:function(n){function o(i){var s;if(i.nodeName==="SELECT")i.focus(),s=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var a=i.hasAttribute("readonly");a||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),a||i.removeAttribute("readonly"),s=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var f=window.getSelection(),c=document.createRange();c.selectNodeContents(i),f.removeAllRanges(),f.addRange(c),s=f.toString()}return s}n.exports=o},279:function(n){function o(){}o.prototype={on:function(i,s,a){var f=this.e||(this.e={});return(f[i]||(f[i]=[])).push({fn:s,ctx:a}),this},once:function(i,s,a){var f=this;function c(){f.off(i,c),s.apply(a,arguments)}return c._=s,this.on(i,c,a)},emit:function(i){var s=[].slice.call(arguments,1),a=((this.e||(this.e={}))[i]||[]).slice(),f=0,c=a.length;for(f;f{"use strict";/*! - * escape-html - * Copyright(c) 2012-2013 TJ Holowaychuk - * Copyright(c) 2015 Andreas Lubbe - * Copyright(c) 2015 Tiancheng "Timothy" Gu - * MIT Licensed - */var rs=/["'&<>]/;Yo.exports=ns;function ns(e){var t=""+e,r=rs.exec(t);if(!r)return t;var n,o="",i=0,s=0;for(i=r.index;i0&&i[i.length-1])&&(c[0]===6||c[0]===2)){r=0;continue}if(c[0]===3&&(!i||c[1]>i[0]&&c[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function W(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var n=r.call(e),o,i=[],s;try{for(;(t===void 0||t-- >0)&&!(o=n.next()).done;)i.push(o.value)}catch(a){s={error:a}}finally{try{o&&!o.done&&(r=n.return)&&r.call(n)}finally{if(s)throw s.error}}return i}function D(e,t,r){if(r||arguments.length===2)for(var n=0,o=t.length,i;n1||a(m,d)})})}function a(m,d){try{f(n[m](d))}catch(h){p(i[0][3],h)}}function f(m){m.value instanceof et?Promise.resolve(m.value.v).then(c,u):p(i[0][2],m)}function c(m){a("next",m)}function u(m){a("throw",m)}function p(m,d){m(d),i.shift(),i.length&&a(i[0][0],i[0][1])}}function pn(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof Ee=="function"?Ee(e):e[Symbol.iterator](),r={},n("next"),n("throw"),n("return"),r[Symbol.asyncIterator]=function(){return this},r);function n(i){r[i]=e[i]&&function(s){return new Promise(function(a,f){s=e[i](s),o(a,f,s.done,s.value)})}}function o(i,s,a,f){Promise.resolve(f).then(function(c){i({value:c,done:a})},s)}}function C(e){return typeof e=="function"}function at(e){var t=function(n){Error.call(n),n.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var It=at(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: -`+r.map(function(n,o){return o+1+") "+n.toString()}).join(` - `):"",this.name="UnsubscriptionError",this.errors=r}});function Ve(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var Ie=function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,n,o,i;if(!this.closed){this.closed=!0;var s=this._parentage;if(s)if(this._parentage=null,Array.isArray(s))try{for(var a=Ee(s),f=a.next();!f.done;f=a.next()){var c=f.value;c.remove(this)}}catch(v){t={error:v}}finally{try{f&&!f.done&&(r=a.return)&&r.call(a)}finally{if(t)throw t.error}}else s.remove(this);var u=this.initialTeardown;if(C(u))try{u()}catch(v){i=v instanceof It?v.errors:[v]}var p=this._finalizers;if(p){this._finalizers=null;try{for(var m=Ee(p),d=m.next();!d.done;d=m.next()){var h=d.value;try{ln(h)}catch(v){i=i!=null?i:[],v instanceof It?i=D(D([],W(i)),W(v.errors)):i.push(v)}}}catch(v){n={error:v}}finally{try{d&&!d.done&&(o=m.return)&&o.call(m)}finally{if(n)throw n.error}}}if(i)throw new It(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)ln(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&Ve(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&Ve(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=function(){var t=new e;return t.closed=!0,t}(),e}();var Sr=Ie.EMPTY;function jt(e){return e instanceof Ie||e&&"closed"in e&&C(e.remove)&&C(e.add)&&C(e.unsubscribe)}function ln(e){C(e)?e():e.unsubscribe()}var Le={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var st={setTimeout:function(e,t){for(var r=[],n=2;n0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var n=this,o=this,i=o.hasError,s=o.isStopped,a=o.observers;return i||s?Sr:(this.currentObservers=null,a.push(r),new Ie(function(){n.currentObservers=null,Ve(a,r)}))},t.prototype._checkFinalizedStatuses=function(r){var n=this,o=n.hasError,i=n.thrownError,s=n.isStopped;o?r.error(i):s&&r.complete()},t.prototype.asObservable=function(){var r=new F;return r.source=this,r},t.create=function(r,n){return new xn(r,n)},t}(F);var xn=function(e){ie(t,e);function t(r,n){var o=e.call(this)||this;return o.destination=r,o.source=n,o}return t.prototype.next=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.next)===null||o===void 0||o.call(n,r)},t.prototype.error=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.error)===null||o===void 0||o.call(n,r)},t.prototype.complete=function(){var r,n;(n=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||n===void 0||n.call(r)},t.prototype._subscribe=function(r){var n,o;return(o=(n=this.source)===null||n===void 0?void 0:n.subscribe(r))!==null&&o!==void 0?o:Sr},t}(x);var Et={now:function(){return(Et.delegate||Date).now()},delegate:void 0};var wt=function(e){ie(t,e);function t(r,n,o){r===void 0&&(r=1/0),n===void 0&&(n=1/0),o===void 0&&(o=Et);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=n,i._timestampProvider=o,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=n===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,n),i}return t.prototype.next=function(r){var n=this,o=n.isStopped,i=n._buffer,s=n._infiniteTimeWindow,a=n._timestampProvider,f=n._windowTime;o||(i.push(r),!s&&i.push(a.now()+f)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var n=this._innerSubscribe(r),o=this,i=o._infiniteTimeWindow,s=o._buffer,a=s.slice(),f=0;f0?e.prototype.requestAsyncId.call(this,r,n,o):(r.actions.push(this),r._scheduled||(r._scheduled=ut.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,n,o){var i;if(o===void 0&&(o=0),o!=null?o>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,n,o);var s=r.actions;n!=null&&((i=s[s.length-1])===null||i===void 0?void 0:i.id)!==n&&(ut.cancelAnimationFrame(n),r._scheduled=void 0)},t}(Wt);var Sn=function(e){ie(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var n=this._scheduled;this._scheduled=void 0;var o=this.actions,i;r=r||o.shift();do if(i=r.execute(r.state,r.delay))break;while((r=o[0])&&r.id===n&&o.shift());if(this._active=!1,i){for(;(r=o[0])&&r.id===n&&o.shift();)r.unsubscribe();throw i}},t}(Dt);var Oe=new Sn(wn);var _=new F(function(e){return e.complete()});function Vt(e){return e&&C(e.schedule)}function Cr(e){return e[e.length-1]}function Ye(e){return C(Cr(e))?e.pop():void 0}function Te(e){return Vt(Cr(e))?e.pop():void 0}function zt(e,t){return typeof Cr(e)=="number"?e.pop():t}var pt=function(e){return e&&typeof e.length=="number"&&typeof e!="function"};function Nt(e){return C(e==null?void 0:e.then)}function qt(e){return C(e[ft])}function Kt(e){return Symbol.asyncIterator&&C(e==null?void 0:e[Symbol.asyncIterator])}function Qt(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function zi(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var Yt=zi();function Gt(e){return C(e==null?void 0:e[Yt])}function Bt(e){return un(this,arguments,function(){var r,n,o,i;return $t(this,function(s){switch(s.label){case 0:r=e.getReader(),s.label=1;case 1:s.trys.push([1,,9,10]),s.label=2;case 2:return[4,et(r.read())];case 3:return n=s.sent(),o=n.value,i=n.done,i?[4,et(void 0)]:[3,5];case 4:return[2,s.sent()];case 5:return[4,et(o)];case 6:return[4,s.sent()];case 7:return s.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function Jt(e){return C(e==null?void 0:e.getReader)}function U(e){if(e instanceof F)return e;if(e!=null){if(qt(e))return Ni(e);if(pt(e))return qi(e);if(Nt(e))return Ki(e);if(Kt(e))return On(e);if(Gt(e))return Qi(e);if(Jt(e))return Yi(e)}throw Qt(e)}function Ni(e){return new F(function(t){var r=e[ft]();if(C(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function qi(e){return new F(function(t){for(var r=0;r=2;return function(n){return n.pipe(e?A(function(o,i){return e(o,i,n)}):de,ge(1),r?He(t):Dn(function(){return new Zt}))}}function Vn(){for(var e=[],t=0;t=2,!0))}function pe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new x}:t,n=e.resetOnError,o=n===void 0?!0:n,i=e.resetOnComplete,s=i===void 0?!0:i,a=e.resetOnRefCountZero,f=a===void 0?!0:a;return function(c){var u,p,m,d=0,h=!1,v=!1,Q=function(){p==null||p.unsubscribe(),p=void 0},B=function(){Q(),u=m=void 0,h=v=!1},N=function(){var O=u;B(),O==null||O.unsubscribe()};return y(function(O,Qe){d++,!v&&!h&&Q();var De=m=m!=null?m:r();Qe.add(function(){d--,d===0&&!v&&!h&&(p=$r(N,f))}),De.subscribe(Qe),!u&&d>0&&(u=new rt({next:function($e){return De.next($e)},error:function($e){v=!0,Q(),p=$r(B,o,$e),De.error($e)},complete:function(){h=!0,Q(),p=$r(B,s),De.complete()}}),U(O).subscribe(u))})(c)}}function $r(e,t){for(var r=[],n=2;ne.next(document)),e}function K(e,t=document){return Array.from(t.querySelectorAll(e))}function z(e,t=document){let r=ce(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function ce(e,t=document){return t.querySelector(e)||void 0}function _e(){return document.activeElement instanceof HTMLElement&&document.activeElement||void 0}function tr(e){return L(b(document.body,"focusin"),b(document.body,"focusout")).pipe(ke(1),l(()=>{let t=_e();return typeof t!="undefined"?e.contains(t):!1}),V(e===_e()),J())}function Xe(e){return{x:e.offsetLeft,y:e.offsetTop}}function Kn(e){return L(b(window,"load"),b(window,"resize")).pipe(Ce(0,Oe),l(()=>Xe(e)),V(Xe(e)))}function rr(e){return{x:e.scrollLeft,y:e.scrollTop}}function dt(e){return L(b(e,"scroll"),b(window,"resize")).pipe(Ce(0,Oe),l(()=>rr(e)),V(rr(e)))}var Yn=function(){if(typeof Map!="undefined")return Map;function e(t,r){var n=-1;return t.some(function(o,i){return o[0]===r?(n=i,!0):!1}),n}return function(){function t(){this.__entries__=[]}return Object.defineProperty(t.prototype,"size",{get:function(){return this.__entries__.length},enumerable:!0,configurable:!0}),t.prototype.get=function(r){var n=e(this.__entries__,r),o=this.__entries__[n];return o&&o[1]},t.prototype.set=function(r,n){var o=e(this.__entries__,r);~o?this.__entries__[o][1]=n:this.__entries__.push([r,n])},t.prototype.delete=function(r){var n=this.__entries__,o=e(n,r);~o&&n.splice(o,1)},t.prototype.has=function(r){return!!~e(this.__entries__,r)},t.prototype.clear=function(){this.__entries__.splice(0)},t.prototype.forEach=function(r,n){n===void 0&&(n=null);for(var o=0,i=this.__entries__;o0},e.prototype.connect_=function(){!Wr||this.connected_||(document.addEventListener("transitionend",this.onTransitionEnd_),window.addEventListener("resize",this.refresh),va?(this.mutationsObserver_=new MutationObserver(this.refresh),this.mutationsObserver_.observe(document,{attributes:!0,childList:!0,characterData:!0,subtree:!0})):(document.addEventListener("DOMSubtreeModified",this.refresh),this.mutationEventsAdded_=!0),this.connected_=!0)},e.prototype.disconnect_=function(){!Wr||!this.connected_||(document.removeEventListener("transitionend",this.onTransitionEnd_),window.removeEventListener("resize",this.refresh),this.mutationsObserver_&&this.mutationsObserver_.disconnect(),this.mutationEventsAdded_&&document.removeEventListener("DOMSubtreeModified",this.refresh),this.mutationsObserver_=null,this.mutationEventsAdded_=!1,this.connected_=!1)},e.prototype.onTransitionEnd_=function(t){var r=t.propertyName,n=r===void 0?"":r,o=ba.some(function(i){return!!~n.indexOf(i)});o&&this.refresh()},e.getInstance=function(){return this.instance_||(this.instance_=new e),this.instance_},e.instance_=null,e}(),Gn=function(e,t){for(var r=0,n=Object.keys(t);r0},e}(),Jn=typeof WeakMap!="undefined"?new WeakMap:new Yn,Xn=function(){function e(t){if(!(this instanceof e))throw new TypeError("Cannot call a class as a function.");if(!arguments.length)throw new TypeError("1 argument required, but only 0 present.");var r=ga.getInstance(),n=new La(t,r,this);Jn.set(this,n)}return e}();["observe","unobserve","disconnect"].forEach(function(e){Xn.prototype[e]=function(){var t;return(t=Jn.get(this))[e].apply(t,arguments)}});var Aa=function(){return typeof nr.ResizeObserver!="undefined"?nr.ResizeObserver:Xn}(),Zn=Aa;var eo=new x,Ca=$(()=>k(new Zn(e=>{for(let t of e)eo.next(t)}))).pipe(g(e=>L(ze,k(e)).pipe(R(()=>e.disconnect()))),X(1));function he(e){return{width:e.offsetWidth,height:e.offsetHeight}}function ye(e){return Ca.pipe(S(t=>t.observe(e)),g(t=>eo.pipe(A(({target:r})=>r===e),R(()=>t.unobserve(e)),l(()=>he(e)))),V(he(e)))}function bt(e){return{width:e.scrollWidth,height:e.scrollHeight}}function ar(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}var to=new x,Ra=$(()=>k(new IntersectionObserver(e=>{for(let t of e)to.next(t)},{threshold:0}))).pipe(g(e=>L(ze,k(e)).pipe(R(()=>e.disconnect()))),X(1));function sr(e){return Ra.pipe(S(t=>t.observe(e)),g(t=>to.pipe(A(({target:r})=>r===e),R(()=>t.unobserve(e)),l(({isIntersecting:r})=>r))))}function ro(e,t=16){return dt(e).pipe(l(({y:r})=>{let n=he(e),o=bt(e);return r>=o.height-n.height-t}),J())}var cr={drawer:z("[data-md-toggle=drawer]"),search:z("[data-md-toggle=search]")};function no(e){return cr[e].checked}function Ke(e,t){cr[e].checked!==t&&cr[e].click()}function Ue(e){let t=cr[e];return b(t,"change").pipe(l(()=>t.checked),V(t.checked))}function ka(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function Ha(){return L(b(window,"compositionstart").pipe(l(()=>!0)),b(window,"compositionend").pipe(l(()=>!1))).pipe(V(!1))}function oo(){let e=b(window,"keydown").pipe(A(t=>!(t.metaKey||t.ctrlKey)),l(t=>({mode:no("search")?"search":"global",type:t.key,claim(){t.preventDefault(),t.stopPropagation()}})),A(({mode:t,type:r})=>{if(t==="global"){let n=_e();if(typeof n!="undefined")return!ka(n,r)}return!0}),pe());return Ha().pipe(g(t=>t?_:e))}function le(){return new URL(location.href)}function ot(e){location.href=e.href}function io(){return new x}function ao(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)ao(e,r)}function M(e,t,...r){let n=document.createElement(e);if(t)for(let o of Object.keys(t))typeof t[o]!="undefined"&&(typeof t[o]!="boolean"?n.setAttribute(o,t[o]):n.setAttribute(o,""));for(let o of r)ao(n,o);return n}function fr(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function so(){return location.hash.substring(1)}function Dr(e){let t=M("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function Pa(e){return L(b(window,"hashchange"),e).pipe(l(so),V(so()),A(t=>t.length>0),X(1))}function co(e){return Pa(e).pipe(l(t=>ce(`[id="${t}"]`)),A(t=>typeof t!="undefined"))}function Vr(e){let t=matchMedia(e);return er(r=>t.addListener(()=>r(t.matches))).pipe(V(t.matches))}function fo(){let e=matchMedia("print");return L(b(window,"beforeprint").pipe(l(()=>!0)),b(window,"afterprint").pipe(l(()=>!1))).pipe(V(e.matches))}function zr(e,t){return e.pipe(g(r=>r?t():_))}function ur(e,t={credentials:"same-origin"}){return ue(fetch(`${e}`,t)).pipe(fe(()=>_),g(r=>r.status!==200?Ot(()=>new Error(r.statusText)):k(r)))}function We(e,t){return ur(e,t).pipe(g(r=>r.json()),X(1))}function uo(e,t){let r=new DOMParser;return ur(e,t).pipe(g(n=>n.text()),l(n=>r.parseFromString(n,"text/xml")),X(1))}function pr(e){let t=M("script",{src:e});return $(()=>(document.head.appendChild(t),L(b(t,"load"),b(t,"error").pipe(g(()=>Ot(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(l(()=>{}),R(()=>document.head.removeChild(t)),ge(1))))}function po(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function lo(){return L(b(window,"scroll",{passive:!0}),b(window,"resize",{passive:!0})).pipe(l(po),V(po()))}function mo(){return{width:innerWidth,height:innerHeight}}function ho(){return b(window,"resize",{passive:!0}).pipe(l(mo),V(mo()))}function bo(){return Y([lo(),ho()]).pipe(l(([e,t])=>({offset:e,size:t})),X(1))}function lr(e,{viewport$:t,header$:r}){let n=t.pipe(ee("size")),o=Y([n,r]).pipe(l(()=>Xe(e)));return Y([r,t,o]).pipe(l(([{height:i},{offset:s,size:a},{x:f,y:c}])=>({offset:{x:s.x-f,y:s.y-c+i},size:a})))}(()=>{function e(n,o){parent.postMessage(n,o||"*")}function t(...n){return n.reduce((o,i)=>o.then(()=>new Promise(s=>{let a=document.createElement("script");a.src=i,a.onload=s,document.body.appendChild(a)})),Promise.resolve())}var r=class extends EventTarget{constructor(n){super(),this.url=n,this.m=i=>{i.source===this.w&&(this.dispatchEvent(new MessageEvent("message",{data:i.data})),this.onmessage&&this.onmessage(i))},this.e=(i,s,a,f,c)=>{if(s===`${this.url}`){let u=new ErrorEvent("error",{message:i,filename:s,lineno:a,colno:f,error:c});this.dispatchEvent(u),this.onerror&&this.onerror(u)}};let o=document.createElement("iframe");o.hidden=!0,document.body.appendChild(this.iframe=o),this.w.document.open(),this.w.document.write(` - - -
-
-

Coverage report: - 46.07% -

- -
- -
-

- coverage.py v7.2.5, - created at 2024-01-05 07:41 +0000 -

-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Modulestatementsmissingexcludedbranchespartialcoverage
src/xlbudget/__init__.py5300040.00%
src/xlbudget/commands.py10634024467.69%
src/xlbudget/configure.py251402040.74%
src/xlbudget/inputformat.py8436032352.59%
src/xlbudget/rwxlb.py199127044032.10%
Total4192140102746.07%
-

- No items found using the specified filter. -

-
- - - diff --git a/pr-24/coverage/d_ebaf54d0d3802af7___init___py.html b/pr-24/coverage/d_ebaf54d0d3802af7___init___py.html deleted file mode 100644 index 37542a9..0000000 --- a/pr-24/coverage/d_ebaf54d0d3802af7___init___py.html +++ /dev/null @@ -1,109 +0,0 @@ - - - - - Coverage for src/xlbudget/__init__.py: 40.00% - - - - - -
- -
-
-

1"""Xlbudget: a personal bookkeeping assistant.""" 

-

2 

-

3from .configure import setup 

-

4 

-

5 

-

6def main(): 

-

7 "Entry point for the application script." 

-

8 args = setup() 

-

9 cmd = args.init(args) 

-

10 cmd.run() 

-
- - - diff --git a/pr-24/coverage/d_ebaf54d0d3802af7_commands_py.html b/pr-24/coverage/d_ebaf54d0d3802af7_commands_py.html deleted file mode 100644 index 0529a40..0000000 --- a/pr-24/coverage/d_ebaf54d0d3802af7_commands_py.html +++ /dev/null @@ -1,379 +0,0 @@ - - - - - Coverage for src/xlbudget/commands.py: 67.69% - - - - - -
-
-

- Coverage for src/xlbudget/commands.py: - 67.69% -

- -

- 106 statements   - - - - -

-

- « prev     - ^ index     - » next -       - coverage.py v7.2.5, - created at 2024-01-05 07:41 +0000 -

- -
-
-
-

1"""The commands, implemented as implementations of the abstract class `Command`.""" 

-

2 

-

3import os 

-

4import sys 

-

5from abc import ABC, abstractmethod 

-

6from argparse import ArgumentParser, Namespace, _SubParsersAction 

-

7from logging import getLogger 

-

8from typing import List, Optional, Type 

-

9 

-

10from openpyxl import Workbook, load_workbook 

-

11 

-

12from xlbudget.inputformat import GetInputFormats, InputFormat, parse_input 

-

13from xlbudget.rwxlb import update_xlbudget 

-

14 

-

15logger = getLogger(__name__) 

-

16 

-

17 

-

18class Command(ABC): 

-

19 """The abstract class that the command implementations implement. 

-

20 

-

21 Attributes: Class Attributes 

-

22 default_path (str): The default path of the xlbudget file. 

-

23 

-

24 Attributes: 

-

25 trial (bool): If True, the xlbudget file will not be written to. 

-

26 path (str): The path to the xlbudget file. 

-

27 """ 

-

28 

-

29 default_path: str = "xlbudget.xlsx" 

-

30 

-

31 @property 

-

32 @abstractmethod 

-

33 def name(self) -> str: 

-

34 """Ensures that the `name` class attribute is defined in subclasses. 

-

35 Part 1/2 of the abstract attribute implementation of `name`. 

-

36 Reference: https://stackoverflow.com/a/53417582. 

-

37 """ 

-

38 raise NotImplementedError 

-

39 

-

40 def get_name(self) -> str: 

-

41 """Used to access the `name` class attribute defined in subclasses. 

-

42 Part 2/2 of the abstract attribute implementation of `name`. 

-

43 Reference: https://stackoverflow.com/a/53417582. 

-

44 """ 

-

45 return self.name 

-

46 

-

47 @property 

-

48 @abstractmethod 

-

49 def aliases(self) -> List[str]: 

-

50 """Ensures that the `aliases` class attribute is defined in subclasses. 

-

51 Part 1/2 of the abstract attribute implementation of `aliases`. 

-

52 Reference: https://stackoverflow.com/a/53417582. 

-

53 """ 

-

54 raise NotImplementedError 

-

55 

-

56 def get_aliases(self) -> List[str]: 

-

57 """Used to access the `aliases` class attribute defined in subclasses. 

-

58 Part 2/2 of the abstract attribute implementation of `aliases`. 

-

59 Reference: https://stackoverflow.com/a/53417582. 

-

60 """ 

-

61 return self.aliases 

-

62 

-

63 @classmethod 

-

64 def configure_common_args(cls, parser: ArgumentParser) -> None: 

-

65 """Configures the arguments that are used by all commands. 

-

66 

-

67 Args: 

-

68 parser (ArgumentParser): The argument parser. 

-

69 """ 

-

70 parser.add_argument( 

-

71 "-t", 

-

72 "--trial", 

-

73 action="store_true", 

-

74 help="try a command without generating/updating the xlbudget file", 

-

75 ) 

-

76 parser.add_argument( 

-

77 "-p", 

-

78 "--path", 

-

79 help="path to the xlbudget file (default: %(default)s)", 

-

80 default=cls.default_path, 

-

81 ) 

-

82 

-

83 @classmethod 

-

84 @abstractmethod 

-

85 def configure_args(cls, subparsers: _SubParsersAction) -> None: 

-

86 pass 

-

87 

-

88 @abstractmethod 

-

89 def __init__(self, args: Namespace) -> None: 

-

90 self.trial = args.trial 

-

91 

-

92 self._check_path(args.path) 

-

93 self.path = args.path 

-

94 

-

95 @staticmethod 

-

96 def _check_path(path: str) -> None: 

-

97 """Check that `path` is a valid path to an xlbudget file. 

-

98 

-

99 Args: 

-

100 path (str): The xlbudget path. 

-

101 

-

102 Raises: 

-

103 ValueError: If `path` is not a XLSX file. 

-

104 FileNotFoundError: If `path` is not in an existing directory. 

-

105 """ 

-

106 xlsx_ext = ".xlsx" 

-

107 if not path.endswith(xlsx_ext): 

-

108 raise ValueError(f"Path '{path}' does not end with '{xlsx_ext}'") 

-

109 

-

110 dir = os.path.dirname(path) 

-

111 if dir and not os.path.isdir(dir): 

-

112 raise FileNotFoundError(f"Directory '{dir}' does not exist") 

-

113 

-

114 @abstractmethod 

-

115 def run(self) -> None: 

-

116 pass 

-

117 

-

118 

-

119class Update(Command): 

-

120 """The `update` command updates an existing xlbudget file. 

-

121 

-

122 Attributes: Class Attributes 

-

123 name (str): The command's CLI name. 

-

124 aliases (List[str]): The command's CLI aliases. 

-

125 

-

126 Attributes: 

-

127 input (Optional[str]): The path to the input file, otherwise paste in terminal. 

-

128 format (inputformat.InputFormat): The input file format. 

-

129 year (Optional[str]): The year all transactions were made, only relevant if 

-

130 the input format is 'BMO_CC_ADOBE'. 

-

131 """ 

-

132 

-

133 name: str = "update" 

-

134 aliases: List[str] = ["u"] 

-

135 

-

136 @classmethod 

-

137 def configure_args(cls, subparsers: _SubParsersAction) -> None: 

-

138 """Configures the argument parser for the `update` command. 

-

139 

-

140 Args: 

-

141 subparsers (_SubParsersAction): The command `subparsers`. 

-

142 """ 

-

143 parser = _add_parser( 

-

144 subparsers, 

-

145 name=cls.name, 

-

146 aliases=cls.aliases, 

-

147 help="update an existing xlbudget file", 

-

148 cmd_cls=Update, 

-

149 ) 

-

150 

-

151 # required arguments 

-

152 parser.add_argument( 

-

153 "format", 

-

154 action=GetInputFormats, 

-

155 choices=GetInputFormats.input_formats.keys(), 

-

156 help="select an input format", 

-

157 ) 

-

158 

-

159 # optional arguments 

-

160 parser.add_argument("-i", "--input", help="path to the input file") 

-

161 parser.add_argument( 

-

162 "-y", 

-

163 "--year", 

-

164 help="year that all transactions were made, only relevant if input format " 

-

165 "is 'BMO_CC_ADOBE'", 

-

166 ) 

-

167 

-

168 def __init__(self, args: Namespace) -> None: 

-

169 super().__init__(args) 

-

170 

-

171 self._check_input(args.input, args.format, args.year) 

-

172 self.input = args.input 

-

173 self.format = args.format 

-

174 self.year = args.year 

-

175 

-

176 logger.debug(f"instance variables: {vars(self)}") 

-

177 

-

178 @staticmethod 

-

179 def _check_input( 

-

180 input: Optional[str], input_format: Optional[InputFormat], year: Optional[str] 

-

181 ) -> None: 

-

182 """Check that `input` and `year` are valid. 

-

183 

-

184 Args: 

-

185 input (Optional[str]): The input path. 

-

186 input_format (Optional[InputFormat]): The input format. 

-

187 year (Optional[str]): The year of all transactions. 

-

188 

-

189 Raises: 

-

190 ValueError: If `input` is not None and the wrong file extension or DNE. 

-

191 ValueError: If `year` is None when `input_format` is 'BMO_CC_ADOBE'. 

-

192 """ 

-

193 if input is None: 193 ↛ 194line 193 didn't jump to line 194, because the condition on line 193 was never true

-

194 return 

-

195 

-

196 in_ext = (".csv", ".tsv", ".txt") 

-

197 if not input.endswith(in_ext): 

-

198 raise ValueError(f"Input '{input}' does not end with one of '{in_ext}'") 

-

199 

-

200 if not os.path.isfile(input): 

-

201 raise ValueError(f"Input '{input}' is not an existing file") 

-

202 

-

203 # get key from value: https://stackoverflow.com/a/13149770 

-

204 if input_format is not None: 

-

205 # validate year 

-

206 format = list(GetInputFormats.input_formats.keys())[ 

-

207 list(GetInputFormats.input_formats.values()).index(input_format) 

-

208 ] 

-

209 if format == "BMO_CC_ADOBE" and year is None: 209 ↛ 210line 209 didn't jump to line 210, because the condition on line 209 was never true

-

210 raise ValueError(f"Must specify 'year' argument when {format=}") 

-

211 

-

212 # validate input file type in more detail 

-

213 if input.endswith(".csv") and not input_format.seperator == ",": 213 ↛ 214line 213 didn't jump to line 214, because the condition on line 213 was never true

-

214 raise ValueError(f"Input file should be CSV for {format=}") 

-

215 

-

216 if input.endswith(".tsv") and not input_format.seperator == "\t": 216 ↛ 217line 216 didn't jump to line 217, because the condition on line 216 was never true

-

217 raise ValueError(f"Input file should be TSV for {format=}") 

-

218 

-

219 def run(self) -> None: 

-

220 logger.info(f"Parsing input {self.input}") 

-

221 df = parse_input(self.input, self.format, self.year) 

-

222 logger.debug(f"input file: {df.shape=}, df.dtypes=\n{df.dtypes}") 

-

223 logger.debug(f"df.head()=\n{df.head()}") 

-

224 

-

225 if os.path.exists(self.path): 

-

226 logger.info(f"Loading xlbudget file {self.path}") 

-

227 wb = load_workbook(self.path) 

-

228 else: 

-

229 logger.warning(f"xlbudget file {self.path} does not exist, creating") 

-

230 wb = Workbook() 

-

231 ws = wb.active 

-

232 # ignore type mismatch of active worksheet 

-

233 wb.remove(ws) # type: ignore[arg-type] 

-

234 

-

235 logger.info("Updating xlbudget file") 

-

236 update_xlbudget(wb, df) 

-

237 

-

238 if not self.trial: 

-

239 logger.info(f"Saving xlbudget file to {self.path}") 

-

240 wb.save(self.path) 

-

241 else: 

-

242 logger.info(f"Trial run: not saving xlbudget file to {self.path}") 

-

243 

-

244 

-

245def get_command_classes() -> List[Type[Command]]: 

-

246 """Gets all classes that implement the `Command` abstract class. 

-

247 

-

248 Returns: 

-

249 A[n] `List[Type[Command]]` of all command classes. 

-

250 """ 

-

251 command_module = sys.modules[__name__] 

-

252 return [getattr(command_module, c.__name__) for c in Command.__subclasses__()] 

-

253 

-

254 

-

255def _add_parser( 

-

256 subparsers: _SubParsersAction, 

-

257 name: str, 

-

258 aliases: List[str], 

-

259 help: str, 

-

260 cmd_cls: Type[Command], 

-

261) -> ArgumentParser: 

-

262 """Adds an argument parser for a command. Any configuration that is common 

-

263 across commands should go here. 

-

264 

-

265 Args: 

-

266 subparsers (_SubParsersAction): The subparsers object. 

-

267 name (str): The command name. 

-

268 aliases (List[str]): The command aliases. 

-

269 help (str): The command help message. 

-

270 cmd_cls (Type[Command]): The command class. 

-

271 

-

272 Returns: 

-

273 A[n] `ArgumentParser` for a command. 

-

274 """ 

-

275 parser = subparsers.add_parser(name, aliases=aliases, help=help) 

-

276 

-

277 # initialize the command with args.init(...) 

-

278 parser.set_defaults(init=cmd_cls) 

-

279 

-

280 return parser 

-
- - - diff --git a/pr-24/coverage/d_ebaf54d0d3802af7_configure_py.html b/pr-24/coverage/d_ebaf54d0d3802af7_configure_py.html deleted file mode 100644 index 912e65d..0000000 --- a/pr-24/coverage/d_ebaf54d0d3802af7_configure_py.html +++ /dev/null @@ -1,196 +0,0 @@ - - - - - Coverage for src/xlbudget/configure.py: 40.74% - - - - - -
-
-

- Coverage for src/xlbudget/configure.py: - 40.74% -

- -

- 25 statements   - - - - -

-

- « prev     - ^ index     - » next -       - coverage.py v7.2.5, - created at 2024-01-05 07:41 +0000 -

- -
-
-
-

1"""The setup and configuration for xlbudget. 

-

2 

-

3Warning: Logger usage in this file 

-

4 

-

5 The logger can only be used after `_configure_logger` is called in `setup`. 

-

6""" 

-

7 

-

8import logging 

-

9from argparse import ArgumentParser, Namespace 

-

10 

-

11from .commands import Command, get_command_classes 

-

12 

-

13 

-

14def setup() -> Namespace: 

-

15 """Package-level setup and configuration. 

-

16 

-

17 Returns: 

-

18 A[n] `Namespace` containing the parsed CLI arguments. 

-

19 """ 

-

20 parser = _configure_argument_parser() 

-

21 args = parser.parse_args() 

-

22 _configure_logger(args.log_level) 

-

23 

-

24 # log args after call to _configure_logger 

-

25 logger = logging.getLogger(__name__) 

-

26 logger.debug(f"parsed CLI arguments: {args}") 

-

27 

-

28 return args 

-

29 

-

30 

-

31def _configure_argument_parser() -> ArgumentParser: 

-

32 """Configures the argument parser for all arguments. 

-

33 

-

34 Returns: 

-

35 A[n] `ArgumentParser` configured for this package. 

-

36 """ 

-

37 parser = ArgumentParser() 

-

38 

-

39 Command.configure_common_args(parser) 

-

40 _configure_logger_args(parser) 

-

41 

-

42 cmd_subparsers = parser.add_subparsers( 

-

43 title="command", 

-

44 required=True, 

-

45 description="The xlbudget command to run.", 

-

46 ) 

-

47 for cmd_cls in get_command_classes(): 

-

48 cmd_cls.configure_args(cmd_subparsers) 

-

49 

-

50 return parser 

-

51 

-

52 

-

53def _configure_logger_args(parser: ArgumentParser) -> None: 

-

54 """Configures the argument parser for logger arguments. 

-

55 The log level configuration was adapted from 

-

56 [this Stack Overflow answer](https://stackoverflow.com/a/20663028). 

-

57 

-

58 Args: 

-

59 parser (ArgumentParser): The argument parser to update. 

-

60 """ 

-

61 group_log = parser.add_argument_group( 

-

62 "logger configuration", 

-

63 description="Arguments that override the default logger configuration.", 

-

64 ) 

-

65 group_log_lvl = group_log.add_mutually_exclusive_group() 

-

66 group_log_lvl.add_argument( 

-

67 "-d", 

-

68 "--debug", 

-

69 help="print lots of debugging statements; can't use with -v/--verbose", 

-

70 action="store_const", 

-

71 dest="log_level", 

-

72 const=logging.DEBUG, 

-

73 default=logging.WARNING, 

-

74 ) 

-

75 group_log_lvl.add_argument( 

-

76 "-v", 

-

77 "--verbose", 

-

78 help="be verbose; can't use with -d/--debug", 

-

79 action="store_const", 

-

80 dest="log_level", 

-

81 const=logging.INFO, 

-

82 ) 

-

83 

-

84 

-

85def _configure_logger(level: int) -> None: 

-

86 """Configures the logger. 

-

87 

-

88 Since this configuration is global, there is no need to return the logger. 

-

89 To use the logger in a file, add `logger = logging.getLogger(__name__)` at the top. 

-

90 

-

91 Args: 

-

92 level (int): The [logging level](https://docs.python.org/3/library/logging.html#logging-levels). 

-

93 """ # noqa 

-

94 logging.basicConfig( 

-

95 level=level, 

-

96 format="%(levelname)s - %(name)s:%(lineno)s - %(message)s", 

-

97 ) 

-
- - - diff --git a/pr-24/coverage/d_ebaf54d0d3802af7_inputformat_py.html b/pr-24/coverage/d_ebaf54d0d3802af7_inputformat_py.html deleted file mode 100644 index d7902b2..0000000 --- a/pr-24/coverage/d_ebaf54d0d3802af7_inputformat_py.html +++ /dev/null @@ -1,390 +0,0 @@ - - - - - Coverage for src/xlbudget/inputformat.py: 52.59% - - - - - -
-
-

- Coverage for src/xlbudget/inputformat.py: - 52.59% -

- -

- 84 statements   - - - - -

-

- « prev     - ^ index     - » next -       - coverage.py v7.2.5, - created at 2024-01-05 07:41 +0000 -

- -
-
-
-

1"""Input file format definitions.""" 

-

2 

-

3import io 

-

4import sys 

-

5from argparse import Action 

-

6from datetime import datetime 

-

7from logging import getLogger 

-

8from typing import Callable, Dict, List, NamedTuple, Optional 

-

9 

-

10import numpy as np 

-

11import pandas as pd 

-

12 

-

13from xlbudget.rwxlb import MONTH_COLUMNS, df_drop_ignores, df_drop_na 

-

14 

-

15logger = getLogger(__name__) 

-

16 

-

17 

-

18class InputFormat(NamedTuple): 

-

19 """Specifies the format of the input file. 

-

20 

-

21 Attributes: 

-

22 header (int): The 0-indexed row of the header in the input file. 

-

23 names (List[str]): The column names. 

-

24 usecols (List[int]): The first len(`MONTH_COLUMNS`) elements are indices of 

-

25 columns that map to `MONTH_COLUMNS`, there may indices after for columns 

-

26 required for post-processing. 

-

27 ignores (List[str]): Ignore transactions that contain with these regex patterns. 

-

28 pre_processing (Callable): The function to call before `pd.read_csv()`. 

-

29 post_processing (Callable): The function to call after `pd.read_csv()`. 

-

30 sep (str): The separator. 

-

31 """ 

-

32 

-

33 header: int 

-

34 names: List[str] 

-

35 usecols: List[int] 

-

36 ignores: List[str] 

-

37 pre_processing: Callable = lambda input, year: input 

-

38 post_processing: Callable = lambda df: df 

-

39 seperator: str = "," 

-

40 

-

41 def get_usecols_names(self): 

-

42 return [self.names[i] for i in self.usecols[:3]] 

-

43 

-

44 

-

45# define pre-processing functions below 

-

46 

-

47 

-

48def bmo_cc_adobe_pre_processing(_input: Optional[str], year: str) -> io.StringIO: 

-

49 """Create CSV from input with each element on a new line. 

-

50 

-

51 Args: 

-

52 _input (Optional[str]): The file to process, if `None` then read from stdin. 

-

53 year (str): The year of all transactions. 

-

54 

-

55 Returns: 

-

56 A[n] `io.StringIO` CSV. 

-

57 """ 

-

58 # get lines from stdin or file 

-

59 if _input is None: 

-

60 lines = [] 

-

61 for line in sys.stdin: 

-

62 lines.append(line.strip()) 

-

63 else: 

-

64 with open(_input) as f: 

-

65 lines = f.read().splitlines() 

-

66 

-

67 rows = [] 

-

68 i = 0 

-

69 is_header = True 

-

70 while i < len(lines): 

-

71 elems = lines[i : i + 4] 

-

72 

-

73 # reformat dates and add year 

-

74 if not is_header: 

-

75 elems[0] = year + datetime.strptime(elems[0], "%b. %d").strftime("-%m-%d") 

-

76 elems[1] = year + datetime.strptime(elems[1], "%b. %d").strftime("-%m-%d") 

-

77 

-

78 # add negative sign to amounts that are not credited (CR on next line) 

-

79 if i + 4 < len(lines) and lines[i + 4] == "CR": 

-

80 i += 5 

-

81 else: 

-

82 if not is_header: 

-

83 elems[-1] = "-" + elems[-1] 

-

84 

-

85 i += 4 

-

86 

-

87 row = "\t".join(elems) + "\n" 

-

88 rows.append(row) 

-

89 

-

90 if is_header: 

-

91 is_header = False 

-

92 

-

93 new_input = "".join(rows) 

-

94 return io.StringIO(new_input) 

-

95 

-

96 

-

97# define post-processing functions below 

-

98 

-

99 

-

100def bmo_acct_web_post_processing(df: pd.DataFrame) -> pd.DataFrame: 

-

101 """Creates the "Amount" column. 

-

102 

-

103 Args: 

-

104 df (pd.DataFrame): The dataframe to process. 

-

105 

-

106 Returns: 

-

107 A[n] `pd.DataFrame` that combines "Amount" and "Money in" to create "Amount". 

-

108 """ 

-

109 df["Amount"] = df["Amount"].replace("[$,]", "", regex=True).astype(float) 

-

110 df["Money in"] = df["Money in"].replace("[$,]", "", regex=True).astype(float) 

-

111 df["Amount"] = np.where(df["Money in"].isna(), df["Amount"], df["Money in"]) 

-

112 df = df.drop("Money in", axis=1) 

-

113 return df 

-

114 

-

115 

-

116def bmo_cc_web_post_processing(df: pd.DataFrame) -> pd.DataFrame: 

-

117 """Formats the "Money in/out" column. 

-

118 

-

119 Args: 

-

120 df (pd.DataFrame): The dataframe to process. 

-

121 

-

122 Returns: 

-

123 A[n] `pd.DataFrame` that converts "Money in/out" to a float. 

-

124 """ 

-

125 df["Money in/out"] = ( 

-

126 df["Money in/out"].replace("[$,]", "", regex=True).astype(float) 

-

127 ) 

-

128 return df 

-

129 

-

130 

-

131# define input formats below 

-

132 

-

133 

-

134BMO_ACCT = InputFormat( 

-

135 header=3, 

-

136 names=[ 

-

137 "First Bank Card", 

-

138 "Transaction Type", 

-

139 "Date Posted", 

-

140 "Transaction Amount", 

-

141 "Description", 

-

142 ], 

-

143 usecols=[2, 4, 3], 

-

144 ignores=[r"^\[CW\] TF.*(?:285|493|593|625)$"], 

-

145) 

-

146 

-

147BMO_ACCT_WEB = InputFormat( 

-

148 header=0, 

-

149 names=[ 

-

150 "Date", 

-

151 "Description", 

-

152 "Amount", # actually named "Money out", but matches after post-processing 

-

153 "Money in", 

-

154 "Balance", 

-

155 ], 

-

156 usecols=[0, 1, 2, 3], 

-

157 ignores=[r"^TF.*(?:285|493|593|625)$"], 

-

158 post_processing=bmo_acct_web_post_processing, 

-

159 seperator="\t", 

-

160) 

-

161 

-

162BMO_CC = InputFormat( 

-

163 header=2, 

-

164 names=[ 

-

165 "Item #", 

-

166 "Card #", 

-

167 "Transaction Date", 

-

168 "Posting Date", 

-

169 "Transaction Amount", 

-

170 "Description", 

-

171 ], 

-

172 usecols=[2, 5, 4], 

-

173 ignores=[r"^TRSF FROM.*(?:285|493|593)$"], 

-

174) 

-

175 

-

176BMO_CC_WEB = InputFormat( 

-

177 header=0, 

-

178 names=[ 

-

179 "Transaction date", 

-

180 "Description", 

-

181 "Money in/out", 

-

182 ], 

-

183 usecols=[0, 1, 2], 

-

184 ignores=[r"^TRSF FROM.*(?:285|493|593)$"], 

-

185 post_processing=bmo_cc_web_post_processing, 

-

186 seperator="\t", 

-

187) 

-

188 

-

189BMO_CC_ADOBE = InputFormat( 

-

190 header=0, 

-

191 names=[ 

-

192 "Transaction Date", 

-

193 "Posting Date", 

-

194 "Description", 

-

195 "Amount", 

-

196 ], 

-

197 usecols=[0, 2, 3], 

-

198 ignores=[r"^TRSF FROM.*(?:285|493|593)$"], 

-

199 pre_processing=bmo_cc_adobe_pre_processing, 

-

200 seperator="\t", 

-

201) 

-

202 

-

203 

-

204# define input formats above 

-

205 

-

206 

-

207class GetInputFormats(Action): 

-

208 """Argparse action for the format argument. 

-

209 Adapted from [this Stack Overflow answer](https://stackoverflow.com/a/50799463). 

-

210 

-

211 Attributes: 

-

212 input_formats (Dict[str, InputFormat]): Maps format names to values. 

-

213 """ 

-

214 

-

215 input_formats: Dict[str, InputFormat] = { 

-

216 n: globals()[n] for n in globals() if isinstance(globals()[n], InputFormat) 

-

217 } 

-

218 

-

219 def __call__(self, parser, namespace, values, option_string=None): 

-

220 setattr(namespace, self.dest, self.input_formats[values]) 

-

221 

-

222 

-

223def parse_input( 

-

224 input: Optional[str], format: InputFormat, year: Optional[str] 

-

225) -> pd.DataFrame: 

-

226 """Parses an input. 

-

227 

-

228 Args: 

-

229 input (Optional[str]): The path to the input file, if None parse from stdin. 

-

230 format (InputFormat): The input format. 

-

231 year (Optional[str]): The year of all transactions. 

-

232 

-

233 Raises: 

-

234 ValueError: If input contains duplicate transactions. 

-

235 

-

236 Returns: 

-

237 A[n] `pd.DataFrame` where the columns match the xlbudget file's column names. 

-

238 """ 

-

239 input_initially_none = input is None 

-

240 if input_initially_none: 240 ↛ 241line 240 didn't jump to line 241, because the condition on line 240 was never true

-

241 print("Paste your transactions here (CTRL+D twice on a blank line to end):") 

-

242 

-

243 input = format.pre_processing(input, year) 

-

244 

-

245 df = pd.read_csv( 

-

246 input if input is not None else sys.stdin, 

-

247 sep=format.seperator, 

-

248 index_col=False, 

-

249 names=format.names, 

-

250 header=format.header if input is not None else None, 

-

251 usecols=format.usecols, 

-

252 parse_dates=[0], 

-

253 skip_blank_lines=False, 

-

254 ) 

-

255 

-

256 if input_initially_none: 256 ↛ 257line 256 didn't jump to line 257, because the condition on line 256 was never true

-

257 print("---End of transactions---") 

-

258 

-

259 df = format.post_processing(df) 

-

260 

-

261 # convert first column to datetime and replace any invalid values with NaT 

-

262 df[df.columns[0]] = pd.to_datetime(df[df.columns[0]], errors="coerce") 

-

263 

-

264 df = df_drop_na(df) 

-

265 

-

266 df.columns = df.columns.str.strip() 

-

267 

-

268 # order columns to match `MONTH_COLUMNS` 

-

269 df = df[format.get_usecols_names()] 

-

270 

-

271 # rename columns to match `MONTH_COLUMNS` 

-

272 df = df.set_axis([c.name for c in MONTH_COLUMNS], axis="columns") 

-

273 

-

274 # sort rows by date 

-

275 df = df.sort_values(by=list(df.columns), ascending=True) 

-

276 

-

277 # strip whitespace from descriptions 

-

278 df["Description"] = df["Description"].str.strip() 

-

279 

-

280 # drop ignored transactions 

-

281 df = df_drop_ignores(df, "|".join(format.ignores)) 

-

282 

-

283 # TODO: write issues to make ignoring identical transactions interactive 

-

284 # TODO: investigate autocompletions 

-

285 if df.duplicated().any(): 285 ↛ 286line 285 didn't jump to line 286, because the condition on line 285 was never true

-

286 logger.warning( 

-

287 "The following transactions are identical:\n" 

-

288 f"{df[df.duplicated(keep=False)]}" 

-

289 ) 

-

290 

-

291 return df 

-
- - - diff --git a/pr-24/coverage/d_ebaf54d0d3802af7_rwxlb_py.html b/pr-24/coverage/d_ebaf54d0d3802af7_rwxlb_py.html deleted file mode 100644 index f0432c2..0000000 --- a/pr-24/coverage/d_ebaf54d0d3802af7_rwxlb_py.html +++ /dev/null @@ -1,496 +0,0 @@ - - - - - Coverage for src/xlbudget/rwxlb.py: 32.10% - - - - - -
-
-

- Coverage for src/xlbudget/rwxlb.py: - 32.10% -

- -

- 199 statements   - - - - -

-

- « prev     - ^ index     - » next -       - coverage.py v7.2.5, - created at 2024-01-05 07:41 +0000 -

- -
-
-
-

1"""xlbudget file reading and writing.""" 

-

2 

-

3import calendar 

-

4from logging import getLogger 

-

5from typing import Dict, List, NamedTuple 

-

6 

-

7import pandas as pd 

-

8from openpyxl import Workbook 

-

9from openpyxl.chart import BarChart, Reference 

-

10from openpyxl.styles import Alignment, Font 

-

11from openpyxl.utils import get_column_letter 

-

12from openpyxl.utils.cell import column_index_from_string, coordinate_from_string 

-

13from openpyxl.worksheet.table import Table, TableStyleInfo 

-

14from openpyxl.worksheet.worksheet import Worksheet 

-

15 

-

16logger = getLogger(__name__) 

-

17 

-

18FORMAT_ACCOUNTING = '_($* #,##0.00_);_($* (#,##0.00);_($* "-"??_);_(@_)' 

-

19FORMAT_DATE = "MM/DD/YYYY" 

-

20FORMAT_NUMBER = '_($* #,##0_);_($* (#,##0);_($* "-"??_);_(@_)' 

-

21 

-

22MONTH_NAME_0_IND = calendar.month_name[1:] 

-

23MONTH_TABLES_ROW = 17 

-

24MONTH_TABLES_COL = 6 

-

25 

-

26 

-

27class ColumnSpecs(NamedTuple): 

-

28 name: str 

-

29 format: str 

-

30 width: int 

-

31 

-

32 

-

33MONTH_COLUMNS = [ 

-

34 ColumnSpecs(name="Date", format=FORMAT_DATE, width=12), 

-

35 ColumnSpecs(name="Description", format="", width=20), 

-

36 ColumnSpecs(name="Amount", format=FORMAT_ACCOUNTING, width=12), 

-

37] 

-

38SUMMARY_COLUMNS = [ 

-

39 ColumnSpecs(name="Month", format="", width=12), 

-

40 ColumnSpecs(name="Incomes", format=FORMAT_ACCOUNTING, width=12), 

-

41 ColumnSpecs(name="Expenses", format=FORMAT_ACCOUNTING, width=12), 

-

42 ColumnSpecs(name="Net", format=FORMAT_ACCOUNTING, width=12), 

-

43] 

-

44 

-

45 

-

46class TablePosition: 

-

47 """The state and bounds of a worksheet table. 

-

48 Read-only fields were implemented with properties that return mangled variables. 

-

49 """ 

-

50 

-

51 def __init__(self, ref: str) -> None: 

-

52 # excel ref format: "<top left cell coordinate>:<bottom right cell coordinate>" 

-

53 start, end = ref.split(":") 

-

54 

-

55 self.__first_col, self.__header_row = coordinate_from_string(start) 

-

56 self.next_row = self.__header_row + 1 

-

57 self.__first_col_ind = column_index_from_string(self.__first_col) 

-

58 

-

59 self.__last_col, self.__initial_last_row = coordinate_from_string(end) 

-

60 

-

61 @property 

-

62 def header_row(self) -> int: 

-

63 return self.__header_row 

-

64 

-

65 @property 

-

66 def first_col(self) -> int: 

-

67 return self.__first_col_ind 

-

68 

-

69 @property 

-

70 def initial_last_row(self) -> int: 

-

71 return self.__initial_last_row 

-

72 

-

73 def __repr__(self) -> str: 

-

74 return ( 

-

75 f"{self.__class__.__name__}(next_row={self.next_row}, " 

-

76 f"first_col={self.first_col}, initial_last_row={self.initial_last_row}, " 

-

77 f"header_row={self.header_row})" 

-

78 ) 

-

79 

-

80 def get_ref(self) -> str: 

-

81 # Excel tables must have at least 2 rows: 1 header and 1+ data. `last_row` is 

-

82 # implemented as follows so that `next_row` can be incremented consistently. 

-

83 last_row = ( 

-

84 self.next_row - 1 

-

85 if self.next_row - 1 >= self.header_row + 1 

-

86 else self.header_row + 1 

-

87 ) 

-

88 return f"{self.__first_col}{self.header_row}:{self.__last_col}{last_row}" 

-

89 

-

90 

-

91def create_year_sheet(wb: Workbook, year: int) -> None: 

-

92 """Creates a year sheet, with a table for each month. 

-

93 

-

94 Args: 

-

95 wb (openpyxl.workbook.workbook.Workbook): The workbook to create the sheet in. 

-

96 year (int): The year. 

-

97 

-

98 Raises: 

-

99 ValueError: If year sheet `year` already exists in the workbook `wb`. 

-

100 """ 

-

101 index = 0 

-

102 year_str = str(year) 

-

103 if year_str in wb.sheetnames: 

-

104 raise ValueError(f"Year sheet {year_str} already exists") 

-

105 

-

106 logger.debug(f"Creating sheet {year_str} at {index=}") 

-

107 ws = wb.create_sheet(year_str, index) 

-

108 num_tables = len(MONTH_NAME_0_IND) 

-

109 

-

110 for c_start in range( 

-

111 MONTH_TABLES_COL, 

-

112 (len(MONTH_COLUMNS) + 1) * num_tables + MONTH_TABLES_COL, 

-

113 len(MONTH_COLUMNS) + 1, 

-

114 ): 

-

115 month_ind = (c_start - MONTH_TABLES_COL) // (len(MONTH_COLUMNS) + 1) 

-

116 month = MONTH_NAME_0_IND[month_ind] 

-

117 table_name = _get_month_table_name(month, year_str) 

-

118 logger.debug(f"creating {table_name} table") 

-

119 

-

120 _add_table( 

-

121 ws, table_name, c_start, r_start=MONTH_TABLES_ROW, columns=MONTH_COLUMNS 

-

122 ) 

-

123 

-

124 logger.debug("Creating summary table") 

-

125 summ_table_name = _get_summary_table_name(year_str) 

-

126 _add_table(ws, summ_table_name, c_start=1, r_start=1, columns=SUMMARY_COLUMNS) 

-

127 summ_tab = ws.tables[summ_table_name] 

-

128 summ_tab_pos = TablePosition(ref=summ_tab.ref) 

-

129 

-

130 for month in MONTH_NAME_0_IND: 

-

131 month_table_name = _get_month_table_name(month, year_str) 

-

132 table_range = f"{month_table_name}[{MONTH_COLUMNS[-1].name}]" 

-

133 

-

134 # set month cell 

-

135 ws.cell(row=summ_tab_pos.next_row, column=summ_tab_pos.first_col).value = month 

-

136 

-

137 # set incomes cell 

-

138 incomes_cell = ws.cell( 

-

139 row=summ_tab_pos.next_row, column=summ_tab_pos.first_col + 1 

-

140 ) 

-

141 incomes_cell.value = f'=SUMIFS({table_range}, {table_range}, ">0")' 

-

142 incomes_cell.number_format = SUMMARY_COLUMNS[1].format 

-

143 

-

144 # set expenses cell 

-

145 expenses_cell = ws.cell( 

-

146 row=summ_tab_pos.next_row, column=summ_tab_pos.first_col + 2 

-

147 ) 

-

148 expenses_cell.value = f'=-SUMIFS({table_range}, {table_range}, "<=0")' 

-

149 expenses_cell.number_format = SUMMARY_COLUMNS[2].format 

-

150 

-

151 # set net cell 

-

152 net_cell = ws.cell(row=summ_tab_pos.next_row, column=summ_tab_pos.first_col + 3) 

-

153 net_cell.value = f"={incomes_cell.coordinate}-{expenses_cell.coordinate}" 

-

154 net_cell.number_format = SUMMARY_COLUMNS[3].format 

-

155 

-

156 summ_tab_pos.next_row += 1 

-

157 

-

158 summ_tab.ref = summ_tab_pos.get_ref() 

-

159 

-

160 # compute totals 

-

161 # set month cell 

-

162 ws.cell(row=summ_tab_pos.next_row, column=summ_tab_pos.first_col).value = "Total" 

-

163 

-

164 # set other cells and create charts 

-

165 for i in range(1, len(SUMMARY_COLUMNS)): 

-

166 cell = ws.cell(row=summ_tab_pos.next_row, column=summ_tab_pos.first_col + i) 

-

167 cell.value = f"=SUM({summ_table_name}[{SUMMARY_COLUMNS[i].name}])" 

-

168 cell.number_format = SUMMARY_COLUMNS[i].format 

-

169 

-

170 chart = BarChart() 

-

171 data = Reference( 

-

172 ws, 

-

173 min_col=i + 1, 

-

174 min_row=summ_tab_pos.header_row, 

-

175 max_row=summ_tab_pos.next_row - 1, 

-

176 ) 

-

177 cats = Reference( 

-

178 ws, 

-

179 min_col=summ_tab_pos.first_col, 

-

180 min_row=summ_tab_pos.header_row + 1, 

-

181 max_row=summ_tab_pos.next_row - 1, 

-

182 ) 

-

183 chart.add_data(data, titles_from_data=True) 

-

184 chart.set_categories(cats) 

-

185 chart.legend = None 

-

186 chart.y_axis.numFmt = FORMAT_NUMBER 

-

187 chart.height = 7.5 

-

188 chart.width = 8.5 # type: ignore[assignment] 

-

189 start_col = MONTH_TABLES_COL + (i - 1) * (len(MONTH_COLUMNS) + 1) 

-

190 anchor = f"{get_column_letter(start_col)}1" 

-

191 ws.add_chart(chart, anchor) 

-

192 

-

193 

-

194def _add_table( 

-

195 ws: Worksheet, 

-

196 table_name: str, 

-

197 c_start: int, 

-

198 r_start: int, 

-

199 columns: List[ColumnSpecs], 

-

200): 

-

201 # table title 

-

202 table_title = ws.cell(row=r_start, column=c_start) 

-

203 table_title.value = table_name 

-

204 table_title.font = Font(bold=True) 

-

205 table_title.alignment = Alignment(horizontal="center") 

-

206 ws.merge_cells( 

-

207 start_row=r_start, 

-

208 start_column=c_start, 

-

209 end_row=r_start, 

-

210 end_column=c_start + len(columns) - 1, 

-

211 ) 

-

212 

-

213 # table header and formating 

-

214 header_row = r_start + 1 

-

215 transactions_row = r_start + 2 

-

216 for i in range(len(columns)): 

-

217 c = c_start + i 

-

218 

-

219 # header 

-

220 ws.cell(row=header_row, column=c).value = columns[i].name 

-

221 

-

222 # column format 

-

223 cell = ws.cell(row=transactions_row, column=c) 

-

224 if columns[i].format: 

-

225 cell.number_format = columns[i].format 

-

226 

-

227 # column width 

-

228 ws.column_dimensions[get_column_letter(c)].width = columns[i].width 

-

229 

-

230 # create table 

-

231 c_start_ltr = get_column_letter(c_start) 

-

232 c_end_ltr = get_column_letter(c_start + len(columns) - 1) 

-

233 ref = f"{c_start_ltr}{header_row}:{c_end_ltr}{transactions_row}" 

-

234 logger.debug(f"creating table {table_name} with {ref=}") 

-

235 tab = Table(displayName=table_name, ref=ref) 

-

236 

-

237 # add a default style with striped rows and banded columns 

-

238 style = TableStyleInfo( 

-

239 name="TableStyleMedium9", 

-

240 showFirstColumn=False, 

-

241 showLastColumn=False, 

-

242 showRowStripes=True, 

-

243 showColumnStripes=True, 

-

244 ) 

-

245 tab.tableStyleInfo = style 

-

246 

-

247 ws.add_table(tab) 

-

248 

-

249 

-

250def update_xlbudget(wb: Workbook, df: pd.DataFrame): 

-

251 """Updates an xlbudget file. 

-

252 

-

253 Args: 

-

254 wb (openpyxl.workbook.workbook.Workbook): The xlbudget workbook. 

-

255 df (pd.DataFrame): The input file dataframe. 

-

256 """ 

-

257 oldest_date, newest_date = df[df.columns[0]].agg(["min", "max"]) 

-

258 logger.debug(f"{oldest_date=}, {newest_date=}") 

-

259 

-

260 # create year sheets as needed 

-

261 for year in range(oldest_date.year, newest_date.year + 1): 

-

262 if str(year) not in wb.sheetnames: 

-

263 logger.info(f"Creating {year} sheet") 

-

264 create_year_sheet(wb, year) 

-

265 

-

266 # initialize table positions dictionary 

-

267 # maps worksheet names to dictionaries that map table names to their position. 

-

268 table_pos: Dict[str, Dict[str, TablePosition]] = {} 

-

269 for year in range(oldest_date.year, newest_date.year + 1): 

-

270 sheet_name = str(year) 

-

271 table_pos[sheet_name] = {} 

-

272 

-

273 start_month = oldest_date.month if year == oldest_date.year else 1 

-

274 end_month = newest_date.month if year == newest_date.year else 12 

-

275 for month in range(start_month, end_month + 1): 

-

276 month_name = calendar.month_name[month] 

-

277 table_name = _get_month_table_name(month=month_name, year=sheet_name) 

-

278 logger.debug(f"Initializing table {table_name} in sheet {sheet_name}") 

-

279 ref = wb[sheet_name].tables[table_name].ref 

-

280 table_pos[sheet_name][table_name] = TablePosition(ref) 

-

281 

-

282 # update df with transactions in wb 

-

283 logger.debug(f"{df.shape=} before checking existing transactions") 

-

284 for sheet_name in table_pos.keys(): 

-

285 ws = wb[sheet_name] 

-

286 

-

287 for pos in table_pos[sheet_name].values(): 

-

288 is_populated = bool(ws.cell(row=pos.next_row, column=pos.first_col).value) 

-

289 if is_populated: 

-

290 for r in range(pos.next_row, pos.initial_last_row + 1): 

-

291 transaction = [] 

-

292 for i in range(len(MONTH_COLUMNS)): 

-

293 c = pos.first_col + i 

-

294 transaction.append(ws.cell(row=r, column=c).value) 

-

295 

-

296 logger.debug(f"Appending {transaction=} to dataframe") 

-

297 # ignore mypy error and implicitly cast to df.dtypes 

-

298 df.loc[len(df) + 1] = transaction # type: ignore[call-overload] 

-

299 df = df_drop_duplicates(df) 

-

300 # re-sort transactions to make the oldest transactions come first 

-

301 df = df.sort_values(by=list(df.columns), ascending=True) 

-

302 logger.debug(f"{df.shape=} after checking existing transactions") 

-

303 

-

304 # write dataframe to wb 

-

305 for row in df.itertuples(index=False): 

-

306 logger.debug(f"Writing transaction {row} to workbook") 

-

307 

-

308 # get worksheet and table position 

-

309 sheet_name, month_name = str(row.Date.year), calendar.month_name[row.Date.month] 

-

310 table_name = _get_month_table_name(month=month_name, year=sheet_name) 

-

311 ws, pos = wb[sheet_name], table_pos[sheet_name][table_name] 

-

312 

-

313 # set date cell 

-

314 date_cell = ws.cell(row=pos.next_row, column=pos.first_col) 

-

315 date_cell.value = row.Date 

-

316 date_cell.number_format = MONTH_COLUMNS[0].format 

-

317 

-

318 # set description cell 

-

319 ws.cell(row=pos.next_row, column=pos.first_col + 1).value = row.Description 

-

320 

-

321 # set amount cell 

-

322 amount_cell = ws.cell(row=pos.next_row, column=pos.first_col + 2) 

-

323 amount_cell.value = row.Amount 

-

324 amount_cell.number_format = MONTH_COLUMNS[2].format 

-

325 

-

326 pos.next_row += 1 

-

327 

-

328 # update table refs 

-

329 for sheet_name in table_pos.keys(): 

-

330 for table_name, pos in table_pos[sheet_name].items(): 

-

331 tab = wb[sheet_name].tables[table_name] 

-

332 ref = pos.get_ref() 

-

333 if ref != tab.ref: 

-

334 logger.debug( 

-

335 f"Updating ref of table {tab.name} from {tab.ref} to {ref}" 

-

336 ) 

-

337 tab.ref = pos.get_ref() 

-

338 

-

339 

-

340def df_drop_duplicates(df: pd.DataFrame) -> pd.DataFrame: 

-

341 """Checks for duplicate rows, dropping them in place if any. 

-

342 

-

343 Args: 

-

344 df (pd.DataFrame): The original dataframe. 

-

345 

-

346 Returns: 

-

347 A[n] `pd.DataFrame` without any duplicate rows. 

-

348 """ 

-

349 duplicated = df.duplicated() 

-

350 duplicates = df[duplicated] 

-

351 if not duplicates.empty: 

-

352 logger.warning(f"Dropping duplicate transactions:\n{duplicates}") 

-

353 return df[~duplicated] 

-

354 return df 

-

355 

-

356 

-

357def df_drop_ignores(df: pd.DataFrame, ignore: str) -> pd.DataFrame: 

-

358 """Checks for rows containing `ignore`, dropping them in place if any. 

-

359 

-

360 Args: 

-

361 df (pd.DataFrame): The original dataframe. 

-

362 ignore (str): The regex pattern that is in descriptions to ignore. 

-

363 

-

364 Returns: 

-

365 A[n] `pd.DataFrame` without any rows containing `ignore`. 

-

366 """ 

-

367 ignored = df["Description"].str.contains(ignore) 

-

368 ignores = df[ignored] 

-

369 if not ignores.empty: 

-

370 logger.warning(f"Dropping ignored transactions:\n{ignores}") 

-

371 return df[~ignored].reset_index(drop=True) 

-

372 return df 

-

373 

-

374 

-

375def df_drop_na(df: pd.DataFrame) -> pd.DataFrame: 

-

376 """Checks for rows that contain only `na` values, dropping them in place if any. 

-

377 

-

378 Args: 

-

379 df (pd.DataFrame): The original dataframe. 

-

380 

-

381 Returns: 

-

382 A[n] `pd.DataFrame` without any rows that are entirely `na`. 

-

383 """ 

-

384 na = df.isna().all(axis=1) 

-

385 nas = df[na] 

-

386 if not nas.empty: 

-

387 logger.info(f"Dropping rows that contain only `na` values:\n{nas}") 

-

388 return df[~na].reset_index(drop=True) 

-

389 return df 

-

390 

-

391 

-

392def _get_month_table_name(month: str, year: str): 

-

393 return f"_{month}{year}" 

-

394 

-

395 

-

396def _get_summary_table_name(year: str): 

-

397 return f"_Summary{year}" 

-
- - - diff --git a/pr-24/coverage/favicon_32.png b/pr-24/coverage/favicon_32.png deleted file mode 100644 index 8649f0475d8d20793b2ec431fe25a186a414cf10..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1732 zcmV;#20QtQP)K2KOkBOVxIZChq#W-v7@TU%U6P(wycKT1hUJUToW3ke1U1ONa4 z000000000000000bb)GRa9mqwR9|UWHy;^RUrt?IT__Y0JUcxmBP0(51q1>E00030 z|NrOz)aw7%8sJzM<5^g%z7^qE`}_Ot|JUUG(NUkWzR|7K?Zo%@_v-8G-1N%N=D$;; zw;keH4dGY$`1t4M=HK_s*zm^0#KgqfwWhe3qO_HtvXYvtjgX>;-~C$L`&k>^R)9)7 zdPh2TL^pCnHC#0+_4D)M`p?qp!pq{jO_{8;$fbaflbx`Tn52n|n}8VFRTA1&ugOP< zPd{uvFjz7t*Vot1&d$l-xWCk}s;sQL&#O(Bskh6gqNJv>#iB=ypG1e3K!K4yc7!~M zfj4S*g^zZ7eP$+_Sl07Z646l;%urinP#D8a6TwRtnLIRcI!r4f@bK~9-`~;E(N?Lv zSEst7s;rcxsi~}{Nsytfz@MtUoR*iFc8!#vvx}Umhm4blk(_~MdVD-@dW&>!Nn~ro z_E~-ESVQAj6Wmn;(olz(O&_{U2*pZBc1aYjMh>Dq3z|6`jW`RDHV=t3I6yRKJ~LOX zz_z!!vbVXPqob#=pj3^VMT?x6t(irRmSKsMo1~LLkB&=#j!=M%NP35mfqim$drWb9 zYIb>no_LUwc!r^NkDzs4YHu@=ZHRzrafWDZd1EhEVq=tGX?tK$pIa)DTh#bkvh!J- z?^%@YS!U*0E8$q$_*aOTQ&)Ra64g>ep;BdcQgvlg8qQHrP*E$;P{-m=A*@axn@$bO zO-Y4JzS&EAi%YG}N?cn?YFS7ivPY=EMV6~YH;+Xxu|tefLS|Aza)Cg6us#)=JW!uH zQa?H>d^j+YHCtyjL^LulF*05|F$RG!AX_OHVI&MtA~_@=5_lU|0000rbW%=J06GH4 z^5LD8b8apw8vNh1ua1mF{{Hy)_U`NA;Nacc+sCpuHXa-V{r&yz?c(9#+}oX+NmiRW z+W-IqK1oDDR5;6GfCDCOP5}iL5fK(cB~ET81`MFgF2kGa9AjhSIk~-E-4&*tPPKdiilQJ11k_J082ZS z>@TvivP!5ZFG?t@{t+GpR3XR&@*hA_VE1|Lo8@L@)l*h(Z@=?c-NS$Fk&&61IzUU9 z*nPqBM=OBZ-6ka1SJgGAS-Us5EN)r#dUX%>wQZLa2ytPCtMKp)Ob z*xcu38Z&d5<-NBS)@jRD+*!W*cf-m_wmxDEqBf?czI%3U0J$Xik;lA`jg}VH?(S(V zE!M3;X2B8w0TnnW&6(8;_Uc)WD;Ms6PKP+s(sFgO!}B!^ES~GDt4qLPxwYB)^7)XA zZwo9zDy-B0B+jT6V=!=bo(zs_8{eBA78gT9GH$(DVhz;4VAYwz+bOIdZ-PNb|I&rl z^XG=vFLF)1{&nT2*0vMz#}7^9hXzzf&ZdKlEj{LihP;|;Ywqn35ajP?H?7t|i-Un% z&&kxee@9B{nwgv1+S-~0)E1{ob1^Wn`F2isurqThKK=3%&;`@{0{!D- z&CSj80t;uPu&FaJFtSXKH#ajgGj}=sEad7US6jP0|Db@0j)?(5@sf<7`~a9>s;wCa zm^)spe{uxGFmrJYI9cOh7s$>8Npkt-5EWB1UKc`{W{y5Ce$1+nM9Cr;);=Ju#N^62OSlJMn7omiUgP&ErsYzT~iGxcW aE(`!K@+CXylaC4j0000 - - - - - - - - - - - - - - - - - - - - - - - - - - Coverage Report - Xlbudget Docs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
- -
- - - - - - - - -
- - - - - - - -
- -
- - - - -
-
- - - -
-
-
- - - - - - -
-
-
- - - -
-
-
- - - -
-
-
- - - -
-
- - - - - - - - - - - - - - - - - - - - -

Coverage Report

- - - -

-

- - - - - - - -
-
- - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pr-24/coverage/keybd_closed.png b/pr-24/coverage/keybd_closed.png deleted file mode 100644 index ba119c47df81ed2bbd27a06988abf700139c4f99..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9004 zcmeHLc{tSF+aIY=A^R4_poB4tZAN2XC;O7M(inrW3}(h&Q4}dl*&-65$i9^&vW6_# zcM4g`Qix=GhkBl;=lwnJ@Ap2}^}hc-b6vBXb3XUyzR%~}_c`-Dw+!?&>5p(90RRB> zXe~7($~PP3eT?=X<@3~Q1w84vX~IoSx~1#~02+TopXK(db;4v6!{+W`RHLkkHO zo;+s?)puc`+$yOwHv>I$5^8v^F3<|$44HA8AFnFB0cAP|C`p}aSMJK*-CUB{eQ!;K z-9Ju3OQ+xVPr3P#o4>_lNBT;M+1vgV&B~6!naOGHb-LFA9TkfHv1IFA1Y!Iz!Zl3) z%c#-^zNWPq7U_}6I7aHSmFWi125RZrNBKyvnV^?64)zviS;E!UD%LaGRl6@zn!3E{ zJ`B$5``cH_3a)t1#6I7d==JeB_IcSU%=I#DrRCBGm8GvCmA=+XHEvC2SIfsNa0(h9 z7P^C4U`W@@`9p>2f^zyb5B=lpc*RZMn-%%IqrxSWQF8{ec3i?-AB(_IVe z)XgT>Y^u41MwOMFvU=I4?!^#jaS-%bjnx@ zmL44yVEslR_ynm18F!u}Ru#moEn3EE?1=9@$B1Z5aLi5b8{&?V(IAYBzIar!SiY3< z`l0V)djHtrImy}(!7x-Pmq+njM)JFQ9mx*(C+9a3M)(_SW|lrN=gfxFhStu^zvynS zm@gl;>d8i8wpUkX42vS3BEzE3-yctH%t0#N%s+6-&_<*Fe7+h=`=FM?DOg1)eGL~~ zQvIFm$D*lqEh07XrXY=jb%hdyP4)`wyMCb$=-z9(lOme9=tirVkb)_GOl2MJn;=Ky z^0pV1owR7KP-BSxhI@@@+gG0roD-kXE1;!#R7KY1QiUbyDdTElm|ul7{mMdF1%UDJ z_vp=Vo!TCF?D*?u% zk~}4!xK2MSQd-QKC0${G=ZRv2x8%8ZqdfR!?Dv=5Mj^8WU)?iH;C?o6rSQy*^YwQb zf@5V)q=xah#a3UEIBC~N7on(p4jQd4K$|i7k`d8mw|M{Mxapl46Z^X^9U}JgqH#;T z`CTzafpMD+J-LjzF+3Xau>xM_sXisRj6m-287~i9g|%gHc}v77>n_+p7ZgmJszx!b zSmL4wV;&*5Z|zaCk`rOYFdOjZLLQr!WSV6AlaqYh_OE)>rYdtx`gk$yAMO=-E1b~J zIZY6gM*}1UWsJ)TW(pf1=h?lJy_0TFOr|nALGW>$IE1E7z+$`^2WJY+>$$nJo8Rs` z)xS>AH{N~X3+b=2+8Q_|n(1JoGv55r>TuwBV~MXE&9?3Zw>cIxnOPNs#gh~C4Zo=k z&!s;5)^6UG>!`?hh0Q|r|Qbm>}pgtOt23Vh!NSibozH$`#LSiYL)HR4bkfEJMa zBHwC3TaHx|BzD|MXAr>mm&FbZXeEX-=W}Ji&!pji4sO$#0Wk^Q7j%{8#bJPn$C=E% zPlB}0)@Ti^r_HMJrTMN?9~4LQbIiUiOKBVNm_QjABKY4;zC88yVjvB>ZETNzr%^(~ zI3U&Ont?P`r&4 z#Bp)jcVV_N_{c1_qW}_`dQm)D`NG?h{+S!YOaUgWna4i8SuoLcXAZ|#Jh&GNn7B}3 z?vZ8I{LpmCYT=@6)dLPd@|(;d<08ufov%+V?$mgUYQHYTrc%eA=CDUzK}v|G&9}yJ z)|g*=+RH1IQ>rvkY9UIam=fkxWDyGIKQ2RU{GqOQjD8nG#sl+$V=?wpzJdT=wlNWr z1%lw&+;kVs(z?e=YRWRA&jc75rQ~({*TS<( z8X!j>B}?Bxrrp%wEE7yBefQ?*nM20~+ZoQK(NO_wA`RNhsqVkXHy|sod@mqen=B#@ zmLi=x2*o9rWqTMWoB&qdZph$~qkJJTVNc*8^hU?gH_fY{GYPEBE8Q{j0Y$tvjMv%3 z)j#EyBf^7n)2d8IXDYX2O0S%ZTnGhg4Ss#sEIATKpE_E4TU=GimrD5F6K(%*+T-!o z?Se7^Vm`$ZKDwq+=~jf?w0qC$Kr&R-;IF#{iLF*8zKu8(=#chRO;>x zdM;h{i{RLpJgS!B-ueTFs8&4U4+D8|7nP~UZ@P`J;*0sj^#f_WqT#xpA?@qHonGB& zQ<^;OLtOG1w#)N~&@b0caUL7syAsAxV#R`n>-+eVL9aZwnlklzE>-6!1#!tVA`uNo z>Gv^P)sohc~g_1YMC;^f(N<{2y5C^;QCEXo;LQ^#$0 zr>jCrdoeXuff!dJ^`#=Wy2Gumo^Qt7BZrI~G+Pyl_kL>is3P0^JlE;Sjm-YfF~I>t z_KeNpK|5U&F4;v?WS&#l(jxUWDarfcIcl=-6!8>^S`57!M6;hZea5IFA@)2+*Rt85 zi-MBs_b^DU8LygXXQGkG+86N7<%M|baM(orG*ASffC`p!?@m{qd}IcYmZyi^d}#Q& zNjk-0@CajpUI-gPm20ERVDO!L8@p`tMJ69FD(ASIkdoLdiRV6h9TPKRz>2WK4upHd z6OZK33EP?`GoJkXh)S035}uLUO$;TlXwNdMg-WOhLB)7a`-%*a9lFmjf6n+4ZmIHN z-V@$ z8PXsoR4*`5RwXz=A8|5;aXKtSHFccj%dG7cO~UBJnt)61K>-uPX)`vu{7fcX6_>zZ zw_2V&Li+7mxbf!f7{Rk&VVyY!UtZywac%g!cH+xh#j$a`uf?XWl<``t`36W;p7=_* zO6uf~2{sAdkZn=Ts@p0>8N8rzw2ZLS@$ibV-c-QmG@%|3gUUrRxu=e*ekhTa+f?8q z3$JVGPr9w$VQG~QCq~Y=2ThLIH!T@(>{NihJ6nj*HA_C#Popv)CBa)+UI-bx8u8zfCT^*1|k z&N9oFYsZEijPn31Yx_yO5pFs>0tOAV=oRx~Wpy5ie&S_449m4R^{LWQMA~}vocV1O zIf#1ZV85E>tvZE4mz~zn{hs!pkIQM;EvZMimqiPAJu-9P@mId&nb$lsrICS=)zU3~ zn>a#9>}5*3N)9;PTMZ)$`5k} z?iG}Rwj$>Y*|(D3S3e&fxhaPHma8@vwu(cwdlaCjX+NIK6=$H4U`rfzcWQVOhp{fnzuZhgCCGpw|p zTi`>cv~xVzdx|^`C0vXdlMwPae3S?>3|7v$e*Bs6-5gS>>FMHk_r2M(ADOV{KV7+6 zA@5Q(mdx%7J}MY}K461iuQ}5GwDGI=Yc&g0MZHu)7gC3{5@QZj6SJl*o0MS2Cl_ia zyK?9QmC9tJ6yn{EA-erJ4wk$+!E#X(s~9h^HOmQ_|6V_s1)k;%9Q6Niw}SyT?jxl4 z;HYz2$Nj$8Q_*Xo`TWEUx^Q9b+ik@$o39`mlY&P}G8wnjdE+Dlj?uL;$aB$n;x zWoh-M_u>9}_Ok@d_uidMqz10zJc}RQijPW3Fs&~1am=j*+A$QWTvxf9)6n;n8zTQW z!Q_J1%apTsJzLF`#^P_#mRv2Ya_keUE7iMSP!ha-WQoo0vZZG?gyR;+4q8F6tL#u< zRj8Hu5f-p1$J;)4?WpGL{4@HmJ6&tF9A5Tc8Trp>;Y>{^s?Q1&bam}?OjsnKd?|Z82aix26wUOLxbEW~E)|CgJ#)MLf_me# zv4?F$o@A~Um)6>HlM0=3Bd-vc91EM}D+t6-@!}O%i*&Wl%@#C8X+?5+nv`oPu!!=5 znbL+Fk_#J_%8vOq^FIv~5N(nk03kyo1p@l|1c+rO^zCG3bk2?|%AF;*|4si1XM<`a z1NY0-8$wv?&129!(g_A1lXR!+pD*1*cF?T~e1d6*G1Fz)jcSaZoKpxtA%FNnKP2jo zLXn@OR#1z@6zuH%mMB98}-t zHJqClsZ!G5xMSgIs_=<8sBePXxfoXsuvy`|buON9BX%s-o>OVLA)k3W=wKnw1?so$ zEjm0aS=zu@Xu#;{A)QTjJ$a9_={++ACkRY*sk3jLk&Fu}RxR<-DXR<`5`$VNG*wJE zidM6VzaQ!M0gbQM98@x@;#0qUS8O)p6mrYwTk*;8J~!ovbY6jon^Ki}uggd3#J5G8 z>awvtF85Y<9yE{Iag}J7O7)1O=ylk^255@XmV5J06-{xaaSNASZoTKKp~$tSxdUI~ zU1RZ&UuW37Ro&_ryj^cSt$Jd&pt|+h!A&dwcr&`S=R5E`=6Tm`+(qGm@$YZ8(8@a$ zXfo@Rwtvm7N3RMmVCb7radAs-@QtCXx^CQ-<)V>QPLZy@jH{#dc4#(y zV)6Hp{ZMz!|NG8!>i01gZMy)G<8Hf2X7e&LH_gOaajW<<^Xi55@OnlY*|S|*TS8;u_nHbv7lgmmZ+Q<5 zi!*lLCJmdpyzl(L${$C?(pVo|oR%r~x_B_ocPePa_);27^=n4L=`toZ;xdBut9rSv z?wDQ7j2I3WQBdhz%X7`2YaG_y|wA!7|s?k;A&WNMLMTZEzCaE^d??E&u?f=ejQBR~|< z)=thyP2(p8r6mt?Ad}tXAP_GvF9|P630I;$1cpQ+Ay7C34hK^ZV3H4kjPV8&NP>G5 zKRDEIBrFl{M#j4mfP0)68&?mqJP1S?2mU0djAGTjDV;wZ?6vplNn~3Hn$nP>%!dMi zz@bnC7zzi&k&s{QDWkf&zgrVXKUJjY3Gv3bL0}S4h>OdgEJ$Q^&p-VAr3J}^a*+rz z!jW7(h*+GuCyqcC{MD(Ovj^!{pB^OKUe|uy&bD?CN>KZrf3?v>>l*xSvnQiH-o^ViN$%FRdm9url;%(*jf5H$*S)8;i0xWHdl>$p);nH9v0)YfW?Vz$! zNCeUbi9`NEg(i^57y=fzM@1o*z*Bf6?QCV>2p9}(BLlYsOCfMjFv1pw1mlo)Py{8v zppw{MDfEeWN+n>Ne~oI7%9cU}mz0r3!es2gNF0t5jkGipjIo2lz;-e)7}Ul_#!eDv zw;#>kI>;#-pyfeu3Fsd^2F@6=oh#8r9;A!G0`-mm7%{=S;Ec(bJ=I_`FodKGQVNEY zmXwr4{9*jpDl%4{ggQZ5Ac z%wYTdl*!1c5^)%^E78Q&)ma|27c6j(a=)g4sGrp$r{jv>>M2 z6y)E5|Aooe!PSfKzvKA>`a6pfK3=E8vL14ksP&f=>gOP?}rG6ye@9ZR3 zJF*vsh*P$w390i!FV~~_Hv6t2Zl<4VUi|rNja#boFt{%q~xGb z(2petq9A*_>~B*>?d?Olx^lmYg4)}sH2>G42RE; diff --git a/pr-24/coverage/keybd_open.png b/pr-24/coverage/keybd_open.png deleted file mode 100644 index a8bac6c9de256626c680f9e9e3f8ee81d9713ecd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9003 zcmeHLc{tST+n?-2i>)FxMv|DtSZA{DOOu_57&BjtZI~H*ma;_1l4LIxB9dJQ*|TO# zN!sjLvQy+8>YUSgf9L)E-g8~=``>Y0!#wx%xj*;)e4hJ$zP?Ym-Z>3679JK52*jqP zscJy|%SHXLGSN|g3$@6f1Az_(`xu?47+^iYt|X!@!3h9Uyj=k>;6<(w94t$&Tmv4vUI0Y(72z4p-=52qQm)ibdMG{Lq zK-QAXj0ngGo#r{-=KfvMuhjI#;F3ml_v?vI<2-B3E&Sb83IPcet8E#VcMLMbDBXp( zietxGS0^|mhdOuNU*! z>lxhuyJ~5HC9jEu^6wu9yggaJEILLJFELe{&yOk3uY^_mY(J*EdTA{CbDHru&S*s5 zFHGCrim@r19P**ASiJAew_7dD+e>cSOtls3Z#(>lZx1iINjrV7NNt%PDNcMkXlA*W z`Bs*%ezf4U5NxJm__K5P?GEB7`Q`04T`~MTc=Sf&%qHuFd;!rn3}>8+-@yEidsy4J zwgV$+ymZ>vxo%s!H&}(*({B{M0j#!`Lt5GDbvmkji<_pajk9^n5DO(1Q=&m;TJ!?& z?dIZM5vQ>Gv(&EdlJNx^(v{pFFPfSP@r^ zUhRTD7bv*AYH`?Gq11M%nz2r;gHNp42jVLD`5tDqtqX8m!12pRUB0&T%w5?UN8u2$ z{33ra^&{S8?zu^Udrw+}HTUH(`Hi#oxx_~8z^KjV88Ir*uZL|Sg~!j^L_s$=4bBRW zop?W3)Xm?LO6n3E9KHt6XpGZ_HN~5oyARM_FU(4I%qcBvz8@9K>nRPh&##*Eoh-~w z_nj&&SNa->_^2rmZKKZTTsb8qBi7eZ+<|^m6k%kJZMtc45f~Vd$|>90cV@0+305_? z$}Q=5?!3a*rg#60fWtWf!9(Na58NEPqWSacwBi#FiX9R?*v-C&eMqb0k&TM0y0Va% zz~=|oCLbfUU9)b69enmUFXBy2)12vO`bS&kb^YOC0g}4%8d0@NbMm6<9C^4VY$)DE z97dE-HVFOL-)`t{@mQPechUcK@>Nbm7VqtmzZyM5U<`U@;RjksVMF8R*E>VhuI zkJSj=K$J!b9wLT59DZFvicVNQpWLaC2991nDs(piR8YcRq>puA}_3int5bZCnSnDDDBIyC`&DN%_Rawgsxlzfrw!$YU zk697D5ny@b5%eg+G2F&np#M_QkwT<~o z=20^H-;eo=m3|I#91GRY0$TY@>nd$|*Y@6PiI*+2I$KO&NY?@M466>Gt%~Lgowk~^JM_8wk%ghs}g}t}vM}#g;++DAjY#7oR5>!9Zb&%tZ@Av?{`s6b=pUPf& z`Ej0w!tuWT?VOSJ(s^!$)o|_8JY0RAMH30nz=QERTWUx%i6hBP9(PAp{ZQXvk!u}#Vab<|7#n z{maX?O+c&it?=GMZ6-mCiq1b`jrvnH%AIwV(c=)Y+Ng zV<#loBasaSDG>p~!~6DW%DmIwBgLM5kIpGHr(+-C2oq1L_i5|QlNU`n4xG_p4P3X+ zRb3J0k2659ugVF3jbY3g*#hm^+qFWErnuOPd#1_kH{$GKT=$ySdOG<2GJTTZieX8- z?SgdRq&e6K0~#g8LaMO>bF{p3>QU`28P6mcPxd#h%a3HMTriHT*5N2RdHdrvo)Hl( z`U&a1G+qKp7@qqMO*C~Dy@6-;0(yrivn$>oJm|n&YNs2%lFk?#rUv7N=CbY!26_#` zOwy)}i?Rp4nN$r%&5zU9O^|X|`}0gh4dooTajuqYy@fN0lYu~6li4||>k%x%XO;xj z5hh>P?#m$1I$s2gk=e^$N7Mm%F()PB*mBjl8#GTm}V z$n>4H{Zn?>tRb54D4BSNiH}riISvV^~kJ4Oqi-Q}*uV!1arYe1u@i3%->Aj(r zIL(E2nn^nhc3)1$LG?M!Z0P!8{kc7jVZ|z31Z9vW;zWG03+NwSV4)_v?8U zWzJng#k|hYcWf&`>pXSb$1J+|*RC+y0H1PLZGt#e5IB@{-e@rJo$|6ec*b&%(FN6?k>rN1-Nr$ z4m|s8prjrxoFseZy3M8c%nY<;8djgwW?!ntbr_BuPh)z_r$EZ(kbFfHIe-m~a@%)q zLHUZt{_ImXka>hsv7(tXD6IvCnD*Y9=OgFxoLemASErKGmb*^Vr}f(jx0bPl+I)E& zdgR_RtTV3aL1y$Y0L5%R`aCZ_j3{hDnOKUvJ-^B&r*-n!H1{M-gxge|1@AvCd1;LQ z&gyHGB7uzB5-;A*PN28V&l6{zV&ytnvv49kQD;x-Jcw{TPutVpBdI*~r2kQt;9y9} zrm;uL{ueR+pCY~(GsbF5WOLs1yA+{d^Nmfm{aCu^(uKBHuPP3>NOHZQeGCtO_(B6)e%e38$iS+A2@EuwaM3TExzF}i&|u$ zKssx-vZFF{(!fLzv#fm`hUWZG5W_HwZrHcibZGYIaTr8bF#XA~Yf^ke%h&0u3Dx%! z^ibu!hA$rmFDYFLiIR1*I%r`O?aUXua(z?Y&59c);yYe5&auIz#2%m$bF*Hyeb18q z{s%|D-an(}lltLeI1PH%zkvDJwfC);yKU+wq>Y~}`Wh1~1YKy!?;AbZMc?c-xx!ID zGU@t4XMu&;EzIlDe3)0mJ*~+gZ-I|7lWVH7XtQ^*7s@OAG%rXhF&W2i7^~4ZIjANP z)iqZodK~wkV=H<3sb9XbJmqa^_fu6Md2TL+@V@LjyB!gdKL)fcuy|X!v>b{(24;h6 zJWY9Lv8*x1KY;xnwHPyvsDJ@ za=nD?=lf8HdL|ib^6{~*M~Z^@X6f4_vccD5U;FmpEMP#m#3a{Hv(qAR7jbY4j^jmY1_kGt2jCr9Hcns@ad#dkAiH(87OC%{OL&%A8E67dds4 zUUa(por`Wt!CH3Hh4y+T!9&*HuNopp&DuC!EBsu2>zv#{TDK;p*zGdw3Q}{Qa3l3P z;iD#9LF=sx7%v`;5kM(4uz1BHUXiwju?VgYWB8vDMa+TeebP^R`85D{{ zc$n4X&Z!+bAB>Phr{s{sU9$^T=t{2+HO8<@oNBifmQ0|Km;F^;iwj#gXkI1ur>(!Z zG@-if3==No%Idh?cck)-zRX2RqlFtoV`vrn=qyc?4xL}sirUxBJ4r!#F?aOvj)juB z%{tu=P8ttd5+4}c=Ud{6@wDYv&cB^kki63NIG@ATX%<^s?;CRDcEa1`cD0Wo0dd{Y z6qjdr3O;ft)T>4e(3iLm_u`QvGhKad%P9zU^Lh8<(*A{x4mEG2wo)t&m&#+lvgmgT zX=0eA>sxXaMJ9`9ydOiNS4<9P-1gH31Wp9bo%!tP$g@wsOnW*#!un#WK&N2z$F93% z)7XXFa=YT;W;+I0qF=FN_Dr$}{`Q67WG7Phqm*HvlkJb*IdK?p`G_u_U_TMccM}%Z z9o(j&Lzg2plsL#1uY|kR zlIJvxnYMIcl8WJUtLEWZ=Jc)J-!GUhx*adO`KdDYV3eE|sbm38a(2si#4)I#TQ{ zu?Gg4M4z6{uc>!WZ(Z|4?1_ml(CD!lWvQIf+81z4K0o}Pq{RyyL8J8^KU+axA#4qy zQ_Hf5_NC-tOOi9sMZFnv)U{y8i$_y>bVIjd zYdd_eZZ%qsKW*^;2wxh(DlFXEIM5O>17AA*?E6crapNmn`L!Jn>AqbENHS$!E&q-T zFo+4DLWSrzdaYa`rye_*o~K22kByy4JzG;|#gQ7C@QCI9JkMy#2(2Fr`Ks(a7O@xQ zvrGC5UmLAPFdMG#Z`W+kDtZAXOA0bEMIr=*Q!fa#N06YRqNk;z^4on3^%f>IEv8Vr zL60-Ew)rk(`mRiv3IpS4>4mi@^GxX`R5ew(n60W&Syt}_o>A)pgE5&E8 zx78ULi@iR42{_udvF!_&adC>f`(&?{`S`^G4hsg;xq4oViQ6kITte;T!WM@^_k;-B zLpb!avBKI!QgmoYY?o2a^F?+Z#*eEd9ik7<*Uqk8Z`^Mqt=+4+d1B;xTx-$WS;2+I zO|PLhqWk+I$Zt%YKlF@o9>2ARqq#A@Bb52^a#Z=0)&8LgZP% zvLw7M+CWwPCk1sR2eGG6T+wj2r>7^(lX?k3vV)7EP$)P82}dHKR0Ndl?LxtNL0!lK zI}|@SQ~@%ML~x}Lh%VqAPOJ^logxQ;Q0Kuv$*HqAH7~01XMmmYE64doj z0dOP&Ap=Dqp-2?`SAXg(2J^eO3;CytR6XHdSXa0h3;}m`{*wopqUP~Oyub7y8&U5O z;RXPi=uW}`Y94?KMc~(#>9W6^Y0Fj&pS3( z&1F|tv?>wjz7teSRSvR~FB(t85%B2UuQo^=N&+ci3&lwQc&G#dB?U#Ha9F4<9xr7h zBPD@Dps>GCX}ORoSQi|yLq#Qr5vV*UoEQ=zjTM7RN}ch1}Yr4mQkNTZ}}B%l(~;?mS?Yyqf^gft3@K-mCDtb{mq zUTl|YXCKf?dRlT2Bn~8 zNJ`0wBY$x>0Z3$OmG6*>Az;WKS>thNbt)y6T5SYptQ`P%b+Oy!-Psp3bv0CFu{+H{ zW!|+@7lT$I0ayx=WJDx7$w79K1@BPq_7qt5XSblw5^=kZyI=sn({MjqP8n+l-yO=r z{~h>Wm<;WSo-Y48oj67^y5TwBJ4^92JfB%Xe{oB{A8>LfZyE$s*XRVaQ0XiJAiuJ z{_M5i?1aClV_U2g4k1M?Txn@MwF0GZQcxRdF#w7}NFk8o(kPUK)Q?*Eot;dyrFddV zfRY`x2B`Z??XBH?2A}#-e!_oF#?v0ysVxLj42qC|iisN`#nA|Hw73Lyh(;hFKeik! z3*R|qe_OKb&N+m^pnnxbcITWzYwc8{p}VWA69FLoS*+iR=YPQc;{UTy|C9T#upizk zL|1QWC)-nWJzf57_`d-DU^q*_0WM_Xzf1jB$PZb5c^FZ1{$Zm&;FtHmOoy*0T=2& zf1cErYE6u!67_|g#zsd&6|{Xdx}%mlVs_OuBZEMDId(pKK*_0xsYXVM7DkP6jBXz- zEd)lyY5I@OKCuXih+u*QN7paQfUw6wG;XcaW~qWCo?T2*0>x(MuCfDKSAqe7lXsSc7qm4=p(o#F8`bgRO G%6|bpD&^7u diff --git a/pr-24/coverage/status.json b/pr-24/coverage/status.json deleted file mode 100644 index 8a44f49..0000000 --- a/pr-24/coverage/status.json +++ /dev/null @@ -1 +0,0 @@ -{"format":2,"version":"7.2.5","globals":"d1e815e19097665eb6de99dcba510148","files":{"d_ebaf54d0d3802af7___init___py":{"hash":"578ac02de21c5ac0b83ebd03ecd20cd0","index":{"nums":[2,1,5,0,3,0,0,0],"html_filename":"d_ebaf54d0d3802af7___init___py.html","relative_filename":"src/xlbudget/__init__.py"}},"d_ebaf54d0d3802af7_commands_py":{"hash":"f335f02f1b473820b55cd16da92622fd","index":{"nums":[2,1,106,0,34,24,4,8],"html_filename":"d_ebaf54d0d3802af7_commands_py.html","relative_filename":"src/xlbudget/commands.py"}},"d_ebaf54d0d3802af7_configure_py":{"hash":"646f6bafc49e28e149a95838328c4ef8","index":{"nums":[2,1,25,0,14,2,0,2],"html_filename":"d_ebaf54d0d3802af7_configure_py.html","relative_filename":"src/xlbudget/configure.py"}},"d_ebaf54d0d3802af7_inputformat_py":{"hash":"563311f613254a3b55830c2027c8d420","index":{"nums":[2,1,84,0,36,32,3,19],"html_filename":"d_ebaf54d0d3802af7_inputformat_py.html","relative_filename":"src/xlbudget/inputformat.py"}},"d_ebaf54d0d3802af7_rwxlb_py":{"hash":"f0a4f63b4e1e5c3f87b55bd33a38a253","index":{"nums":[2,1,199,0,127,44,0,38],"html_filename":"d_ebaf54d0d3802af7_rwxlb_py.html","relative_filename":"src/xlbudget/rwxlb.py"}}}} \ No newline at end of file diff --git a/pr-24/coverage/style.css b/pr-24/coverage/style.css deleted file mode 100644 index 11b24c4..0000000 --- a/pr-24/coverage/style.css +++ /dev/null @@ -1,309 +0,0 @@ -@charset "UTF-8"; -/* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ -/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ -/* Don't edit this .css file. Edit the .scss file instead! */ -html, body, h1, h2, h3, p, table, td, th { margin: 0; padding: 0; border: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } - -body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-size: 1em; background: #fff; color: #000; } - -@media (prefers-color-scheme: dark) { body { background: #1e1e1e; } } - -@media (prefers-color-scheme: dark) { body { color: #eee; } } - -html > body { font-size: 16px; } - -a:active, a:focus { outline: 2px dashed #007acc; } - -p { font-size: .875em; line-height: 1.4em; } - -table { border-collapse: collapse; } - -td { vertical-align: top; } - -table tr.hidden { display: none !important; } - -p#no_rows { display: none; font-size: 1.2em; } - -a.nav { text-decoration: none; color: inherit; } - -a.nav:hover { text-decoration: underline; color: inherit; } - -.hidden { display: none; } - -header { background: #f8f8f8; width: 100%; z-index: 2; border-bottom: 1px solid #ccc; } - -@media (prefers-color-scheme: dark) { header { background: black; } } - -@media (prefers-color-scheme: dark) { header { border-color: #333; } } - -header .content { padding: 1rem 3.5rem; } - -header h2 { margin-top: .5em; font-size: 1em; } - -header p.text { margin: .5em 0 -.5em; color: #666; font-style: italic; } - -@media (prefers-color-scheme: dark) { header p.text { color: #aaa; } } - -header.sticky { position: fixed; left: 0; right: 0; height: 2.5em; } - -header.sticky .text { display: none; } - -header.sticky h1, header.sticky h2 { font-size: 1em; margin-top: 0; display: inline-block; } - -header.sticky .content { padding: 0.5rem 3.5rem; } - -header.sticky .content p { font-size: 1em; } - -header.sticky ~ #source { padding-top: 6.5em; } - -main { position: relative; z-index: 1; } - -footer { margin: 1rem 3.5rem; } - -footer .content { padding: 0; color: #666; font-style: italic; } - -@media (prefers-color-scheme: dark) { footer .content { color: #aaa; } } - -#index { margin: 1rem 0 0 3.5rem; } - -h1 { font-size: 1.25em; display: inline-block; } - -#filter_container { float: right; margin: 0 2em 0 0; } - -#filter_container input { width: 10em; padding: 0.2em 0.5em; border: 2px solid #ccc; background: #fff; color: #000; } - -@media (prefers-color-scheme: dark) { #filter_container input { border-color: #444; } } - -@media (prefers-color-scheme: dark) { #filter_container input { background: #1e1e1e; } } - -@media (prefers-color-scheme: dark) { #filter_container input { color: #eee; } } - -#filter_container input:focus { border-color: #007acc; } - -header button { font-family: inherit; font-size: inherit; border: 1px solid; border-radius: .2em; color: inherit; padding: .1em .5em; margin: 1px calc(.1em + 1px); cursor: pointer; border-color: #ccc; } - -@media (prefers-color-scheme: dark) { header button { border-color: #444; } } - -header button:active, header button:focus { outline: 2px dashed #007acc; } - -header button.run { background: #eeffee; } - -@media (prefers-color-scheme: dark) { header button.run { background: #373d29; } } - -header button.run.show_run { background: #dfd; border: 2px solid #00dd00; margin: 0 .1em; } - -@media (prefers-color-scheme: dark) { header button.run.show_run { background: #373d29; } } - -header button.mis { background: #ffeeee; } - -@media (prefers-color-scheme: dark) { header button.mis { background: #4b1818; } } - -header button.mis.show_mis { background: #fdd; border: 2px solid #ff0000; margin: 0 .1em; } - -@media (prefers-color-scheme: dark) { header button.mis.show_mis { background: #4b1818; } } - -header button.exc { background: #f7f7f7; } - -@media (prefers-color-scheme: dark) { header button.exc { background: #333; } } - -header button.exc.show_exc { background: #eee; border: 2px solid #808080; margin: 0 .1em; } - -@media (prefers-color-scheme: dark) { header button.exc.show_exc { background: #333; } } - -header button.par { background: #ffffd5; } - -@media (prefers-color-scheme: dark) { header button.par { background: #650; } } - -header button.par.show_par { background: #ffa; border: 2px solid #bbbb00; margin: 0 .1em; } - -@media (prefers-color-scheme: dark) { header button.par.show_par { background: #650; } } - -#help_panel, #source p .annotate.long { display: none; position: absolute; z-index: 999; background: #ffffcc; border: 1px solid #888; border-radius: .2em; color: #333; padding: .25em .5em; } - -#source p .annotate.long { white-space: normal; float: right; top: 1.75em; right: 1em; height: auto; } - -#help_panel_wrapper { float: right; position: relative; } - -#keyboard_icon { margin: 5px; } - -#help_panel_state { display: none; } - -#help_panel { top: 25px; right: 0; padding: .75em; border: 1px solid #883; color: #333; } - -#help_panel .keyhelp p { margin-top: .75em; } - -#help_panel .legend { font-style: italic; margin-bottom: 1em; } - -.indexfile #help_panel { width: 25em; } - -.pyfile #help_panel { width: 18em; } - -#help_panel_state:checked ~ #help_panel { display: block; } - -kbd { border: 1px solid black; border-color: #888 #333 #333 #888; padding: .1em .35em; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-weight: bold; background: #eee; border-radius: 3px; } - -#source { padding: 1em 0 1em 3.5rem; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; } - -#source p { position: relative; white-space: pre; } - -#source p * { box-sizing: border-box; } - -#source p .n { float: left; text-align: right; width: 3.5rem; box-sizing: border-box; margin-left: -3.5rem; padding-right: 1em; color: #999; } - -@media (prefers-color-scheme: dark) { #source p .n { color: #777; } } - -#source p .n.highlight { background: #ffdd00; } - -#source p .n a { margin-top: -4em; padding-top: 4em; text-decoration: none; color: #999; } - -@media (prefers-color-scheme: dark) { #source p .n a { color: #777; } } - -#source p .n a:hover { text-decoration: underline; color: #999; } - -@media (prefers-color-scheme: dark) { #source p .n a:hover { color: #777; } } - -#source p .t { display: inline-block; width: 100%; box-sizing: border-box; margin-left: -.5em; padding-left: 0.3em; border-left: 0.2em solid #fff; } - -@media (prefers-color-scheme: dark) { #source p .t { border-color: #1e1e1e; } } - -#source p .t:hover { background: #f2f2f2; } - -@media (prefers-color-scheme: dark) { #source p .t:hover { background: #282828; } } - -#source p .t:hover ~ .r .annotate.long { display: block; } - -#source p .t .com { color: #008000; font-style: italic; line-height: 1px; } - -@media (prefers-color-scheme: dark) { #source p .t .com { color: #6a9955; } } - -#source p .t .key { font-weight: bold; line-height: 1px; } - -#source p .t .str { color: #0451a5; } - -@media (prefers-color-scheme: dark) { #source p .t .str { color: #9cdcfe; } } - -#source p.mis .t { border-left: 0.2em solid #ff0000; } - -#source p.mis.show_mis .t { background: #fdd; } - -@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t { background: #4b1818; } } - -#source p.mis.show_mis .t:hover { background: #f2d2d2; } - -@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t:hover { background: #532323; } } - -#source p.run .t { border-left: 0.2em solid #00dd00; } - -#source p.run.show_run .t { background: #dfd; } - -@media (prefers-color-scheme: dark) { #source p.run.show_run .t { background: #373d29; } } - -#source p.run.show_run .t:hover { background: #d2f2d2; } - -@media (prefers-color-scheme: dark) { #source p.run.show_run .t:hover { background: #404633; } } - -#source p.exc .t { border-left: 0.2em solid #808080; } - -#source p.exc.show_exc .t { background: #eee; } - -@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t { background: #333; } } - -#source p.exc.show_exc .t:hover { background: #e2e2e2; } - -@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t:hover { background: #3c3c3c; } } - -#source p.par .t { border-left: 0.2em solid #bbbb00; } - -#source p.par.show_par .t { background: #ffa; } - -@media (prefers-color-scheme: dark) { #source p.par.show_par .t { background: #650; } } - -#source p.par.show_par .t:hover { background: #f2f2a2; } - -@media (prefers-color-scheme: dark) { #source p.par.show_par .t:hover { background: #6d5d0c; } } - -#source p .r { position: absolute; top: 0; right: 2.5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; } - -#source p .annotate { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; color: #666; padding-right: .5em; } - -@media (prefers-color-scheme: dark) { #source p .annotate { color: #ddd; } } - -#source p .annotate.short:hover ~ .long { display: block; } - -#source p .annotate.long { width: 30em; right: 2.5em; } - -#source p input { display: none; } - -#source p input ~ .r label.ctx { cursor: pointer; border-radius: .25em; } - -#source p input ~ .r label.ctx::before { content: "▶ "; } - -#source p input ~ .r label.ctx:hover { background: #e8f4ff; color: #666; } - -@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { background: #0f3a42; } } - -@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { color: #aaa; } } - -#source p input:checked ~ .r label.ctx { background: #d0e8ff; color: #666; border-radius: .75em .75em 0 0; padding: 0 .5em; margin: -.25em 0; } - -@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { background: #056; } } - -@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { color: #aaa; } } - -#source p input:checked ~ .r label.ctx::before { content: "▼ "; } - -#source p input:checked ~ .ctxs { padding: .25em .5em; overflow-y: scroll; max-height: 10.5em; } - -#source p label.ctx { color: #999; display: inline-block; padding: 0 .5em; font-size: .8333em; } - -@media (prefers-color-scheme: dark) { #source p label.ctx { color: #777; } } - -#source p .ctxs { display: block; max-height: 0; overflow-y: hidden; transition: all .2s; padding: 0 .5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; white-space: nowrap; background: #d0e8ff; border-radius: .25em; margin-right: 1.75em; text-align: right; } - -@media (prefers-color-scheme: dark) { #source p .ctxs { background: #056; } } - -#index { font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.875em; } - -#index table.index { margin-left: -.5em; } - -#index td, #index th { text-align: right; width: 5em; padding: .25em .5em; border-bottom: 1px solid #eee; } - -@media (prefers-color-scheme: dark) { #index td, #index th { border-color: #333; } } - -#index td.name, #index th.name { text-align: left; width: auto; } - -#index th { font-style: italic; color: #333; cursor: pointer; } - -@media (prefers-color-scheme: dark) { #index th { color: #ddd; } } - -#index th:hover { background: #eee; } - -@media (prefers-color-scheme: dark) { #index th:hover { background: #333; } } - -#index th[aria-sort="ascending"], #index th[aria-sort="descending"] { white-space: nowrap; background: #eee; padding-left: .5em; } - -@media (prefers-color-scheme: dark) { #index th[aria-sort="ascending"], #index th[aria-sort="descending"] { background: #333; } } - -#index th[aria-sort="ascending"]::after { font-family: sans-serif; content: " ↑"; } - -#index th[aria-sort="descending"]::after { font-family: sans-serif; content: " ↓"; } - -#index td.name a { text-decoration: none; color: inherit; } - -#index tr.total td, #index tr.total_dynamic td { font-weight: bold; border-top: 1px solid #ccc; border-bottom: none; } - -#index tr.file:hover { background: #eee; } - -@media (prefers-color-scheme: dark) { #index tr.file:hover { background: #333; } } - -#index tr.file:hover td.name { text-decoration: underline; color: inherit; } - -#scroll_marker { position: fixed; z-index: 3; right: 0; top: 0; width: 16px; height: 100%; background: #fff; border-left: 1px solid #eee; will-change: transform; } - -@media (prefers-color-scheme: dark) { #scroll_marker { background: #1e1e1e; } } - -@media (prefers-color-scheme: dark) { #scroll_marker { border-color: #333; } } - -#scroll_marker .marker { background: #ccc; position: absolute; min-height: 3px; width: 100%; } - -@media (prefers-color-scheme: dark) { #scroll_marker .marker { background: #444; } } diff --git a/pr-24/developer_guide/contributing/index.html b/pr-24/developer_guide/contributing/index.html deleted file mode 100644 index 28247dc..0000000 --- a/pr-24/developer_guide/contributing/index.html +++ /dev/null @@ -1,1204 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - Contributing - Xlbudget Docs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - - -
- - - - - - - -
- -
- - - - -
-
- - - -
-
-
- - - - - - -
-
-
- - - -
-
-
- - - -
-
-
- - - -
-
- - - - - - - - - - - - - - - - - - - - -

Contributing

-

This repository's infrastructure features pinned dependency management, -a documentation site, an automated release process, GitHub integration, VS Code integration, -and much more.

-

Setup for Local Development

-
    -
  1. Install tox
  2. -
  3. Clone the repository
  4. -
-

Tox

-

tox is used to automate and standardize testing -across local development environments and CI/CD pipelines.

-

Tox Configuration

-

The tox configuration for this repository can be found in tox.ini.

-

Tox Environments

-

Each tox environment accomplishes a specific purpose. -List all tox environments and their descriptions with tox list.

-

Details about each environment are given below:

-
    -
  • py*: for a particular python version, it
      -
    1. checks if the package can be built (may be commented out),
    2. -
    3. runs the linters,
    4. -
    5. runs the test suite, and
    6. -
    7. generates a coverage report source file .coverage
    8. -
    -
  • -
  • check-release: checks that the package is ready to be released
  • -
  • coverage: converts .coverage to human readable formats
      -
    • html: used to create the Coverage Report page
    • -
    • json: used to create the coverage badge in the README
    • -
    -
  • -
  • dev: used to create a development environment with all dependencies installed
      -
    • When in the development environment, the commands that are run in each environment - can be run in your terminal
    • -
    -
  • -
  • docs-build: builds the docs to ensure that they are in a valid state
  • -
  • docs-serve: runs the docs development server
  • -
  • format: runs the formatters
  • -
  • update: updates the dependencies without upgrading
  • -
  • upgrade: updates the dependencies
  • -
-

Running Tox Environments

-
    -
  • tox -e <environment> will run a single environment
  • -
  • tox will run all the default environments as noted by tox list
      -
    • To set an environment as default, add it to envlist in tox.ini
    • -
    -
  • -
-

Known issues running tox environments:

- - - - - - - - - - - - - - - -
EnvironmentIssueSolution
coveragecoverage combine outputs "No data to combine"coverage cannot be run independently, as it needs .coverage from testenv: run tox instead. If you are still getting this error, remove .coverage and rerun.
-

Tox Development Environments

-

The tox devenv command will create a virtual environment and install the environment's -dependencies in it.

- -

Dependencies

-

Dependencies are defined in pyproject.toml. -They are pinned and managed using pip-tools. -The pinned dependencies can be found in requirements/.

-

How to Add or Update Dependencies

-
    -
  1. To add a dependency, add it in pyproject.toml; - where you add the dependency depends on what type of dependency it is:
      -
    • Add project dependencies to the dependencies list
    • -
    • Add environment-specific dependencies to the corresponding list - below [project.optional-dependencies]
    • -
    -
  2. -
  3. Run the update tox environment: tox -e update
      -
    • If you want to upgrade dependencies as well, run this instead: tox -e upgrade
    • -
    -
  4. -
  5. Verify that the tests still pass: tox
  6. -
  7. If you are using the development environment, recreate it: tox devenv -e dev .venv
  8. -
  9. Commit and push the changes
  10. -
- - - - - - -
-
- - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pr-24/developer_guide/docs/index.html b/pr-24/developer_guide/docs/index.html deleted file mode 100644 index 1de3e5c..0000000 --- a/pr-24/developer_guide/docs/index.html +++ /dev/null @@ -1,1095 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - Docs - Xlbudget Docs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - - -
- - - - - - - -
- -
- - - - -
-
- - - -
-
-
- - - - - - -
-
-
- - - -
-
-
- - - -
-
-
- - - -
-
- - - - - - - - - - - - - - - - - - - - -

Docs

-

The Docs were created using Material for MkDocs, -a Markdown static site generator with a material design theme.

-

Running Docs Locally

-
    -
  1. Create and use the dev development environment
  2. -
  3. Run the development server: mkdocs serve -
  4. -
-

Building for Offline Usage

-

To build for offline usage, uncomment the offline plugin in mkdocs.yml -before running mkdocs build. For what this does, refer to -the related Material for Mkdocs docs page.

-

Features

-

Automatic Documentation from Sources

-

mkdocstrings was used to create the Code Reference -section of the Docs.

-
-

incompatible theme.features

-

mkdocstrings is not compatible with -theme.features.navigation.indexes.

-
-

Versioning

-

The Docs site has the following versions:

-
    -
  • Version from branches
      -
    • main: aliased to latest
        -
      • Whenever you are on a version other than latest, a warning will be displayed - above the header
      • -
      -
    • -
    • pr-<pr number>:
        -
      • Created when a pull request is opened
      • -
      • Updated when the pull request commits are modified
      • -
      • Deleted when the pull request is closed
      • -
      -
    • -
    -
  • -
  • Version from releases: <x.x>
  • -
- -
-
- - - Last update: - March 19, 2023 - - - -
- - - - - - -
-
- - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pr-24/developer_guide/github_actions/index.html b/pr-24/developer_guide/github_actions/index.html deleted file mode 100644 index f420c96..0000000 --- a/pr-24/developer_guide/github_actions/index.html +++ /dev/null @@ -1,1259 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - GitHub Actions - Xlbudget Docs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - - -
- - - - - - - -
- -
- - - - -
-
- - - -
-
-
- - - - - - -
-
-
- - - -
-
-
- - - -
-
-
- - - -
-
- - - - - - - - - - - - - - - - - - - - -

GitHub Actions

-

CI Workflow

-

The CI workflow contains the jobs that run in pull requests and pushes to the main branch:

-
    -
  • tests: run tox on various Python versions and operating systems
  • -
  • coverage: generates code coverage reports for the docs site and README status badge
  • -
  • docs-build: builds the docs site
  • -
  • markdownlint: lints Markdown files
  • -
  • markdown-link-check: checks links in Markdown files work
  • -
  • docs-deploy and docs-delete: automate docs versioning
  • -
-

Notes:

-
    -
  • tox is used to ensure that results can be replicated locally
  • -
  • Not all jobs run every workflow call: see the if keyword in each job
  • -
  • Some jobs depend on other jobs: see the needs keyword in each job
  • -
  • Artifacts are used to access the coverage reports generated by tests in coverage: - storing workflow data as artifacts
  • -
-
-CI workflow source code -

.github/workflows/ci.yml

-
name: CI
-
-on:
-  pull_request:
-    # default types + closed
-    types: [opened, synchronize, reopened, closed]
-  push:
-    branches:
-      - main
-
-defaults:
-  run:
-    shell: bash
-
-env:
-  PIP_DISABLE_PIP_VERSION_CHECK: 1
-
-permissions:
-  contents: write
-
-jobs:
-  tests:
-    name: "Run tests using python-${{ matrix.python-version }} on ${{ matrix.os }}"
-    runs-on: "${{ matrix.os }}"
-    if: github.event_name != 'pull_request' || github.event.action != 'closed'
-
-    strategy:
-      fail-fast: false
-      matrix:
-        os: [ubuntu-latest, macos-latest, windows-latest]
-        python-version: ['3.8', '3.9', '3.10', '3.11']
-
-    steps:
-      - name: "Check out the repo"
-        uses: "actions/checkout@v4"
-
-      - name: "Set up Python"
-        uses: "actions/setup-python@v5"
-        with:
-          python-version: "${{ matrix.python-version }}"
-          cache: pip
-          cache-dependency-path: '**/requirements/test.txt'
-
-      - name: "Install dependencies"
-        run: python -m pip install tox tox-gh-actions
-
-      - name: "Run tox for python-${{ matrix.python-version }} on ${{ matrix.os }}"
-        run: python -m tox
-
-      - name: "Combine coverage data from tests"
-        if: (matrix.python-version == '3.10') && (matrix.os == 'ubuntu-latest')
-        run: |
-          python -m tox -e coverage
-          export TOTAL=$(python -c "import json;print(json.load(open('coverage.json'))['totals']['percent_covered_display'])")
-          echo "total=$TOTAL" >> $GITHUB_ENV
-          echo "### Total coverage: ${TOTAL}%" >> $GITHUB_STEP_SUMMARY
-
-      - name: "Make coverage badge"
-        if: (matrix.python-version == '3.10') && (matrix.os == 'ubuntu-latest') && (github.repository == 'patrick-5546/xlbudget') && (github.ref == 'refs/heads/main')
-        # https://gist.github.com/patrick-5546/845b19d91f3d03c94677f6fae6eb414c
-        uses: schneegans/dynamic-badges-action@v1.7.0
-        with:
-          # GIST_TOKEN is a GitHub personal access token with scope "gist".
-          auth: ${{ secrets.GIST_TOKEN }}
-          gistID: 845b19d91f3d03c94677f6fae6eb414c   # replace with your real Gist id.
-          filename: covbadge-xlbudget.json
-          label: Coverage
-          message: ${{ env.total }}%
-          minColorRange: 50
-          maxColorRange: 90
-          valColorRange: ${{ env.total }}
-
-      - name: "Upload HTML coverage report"
-        if: (matrix.python-version == '3.10') && (matrix.os == 'ubuntu-latest')
-        uses: actions/upload-artifact@v4
-        with:
-          name: htmlcov
-          path: htmlcov/
-
-  docs-build:
-    name: Build Docs
-    runs-on: ubuntu-latest
-    if: github.event_name != 'pull_request' || github.event.action != 'closed'
-    steps:
-      - name: "Check out the repo"
-        uses: "actions/checkout@v4"
-
-      - name: "Set up Python"
-        uses: "actions/setup-python@v5"
-        with:
-          python-version: "3.10"
-          cache: pip
-          cache-dependency-path: '**/requirements/docs.txt'
-
-      - name: "Install dependencies"
-        run: python -m pip install tox tox-gh-actions
-
-      - name: "Build docs"
-        run: python -m tox -e docs-build
-
-  # https://github.com/nosborn/github-action-markdown-cli
-  markdownlint:
-    name: Lint Markdown
-    runs-on: ubuntu-latest
-    if: github.event_name != 'pull_request' || github.event.action != 'closed'
-    needs: docs-build
-    steps:
-    - name: Check out code
-      uses: actions/checkout@v4
-
-    - name: Lint markdown pages
-      uses: nosborn/github-action-markdown-cli@v3
-      with:
-        files: .
-        config_file: '.markdownlint.json'
-        dot: true
-
-  # https://github.com/gaurav-nelson/github-action-markdown-link-check
-  markdown-link-check:
-    name: Check links in Markdown files
-    runs-on: ubuntu-latest
-    if: github.event_name != 'pull_request' || github.event.action != 'closed'
-    needs: docs-build
-    steps:
-    - name: Check out code
-      uses: actions/checkout@v4
-
-    - name: Check markdown pages for broken links
-      uses: gaurav-nelson/github-action-markdown-link-check@v1
-      with:
-        config-file: '.mlc_config.json'
-        folder-path: '.'
-
-  # https://squidfunk.github.io/mkdocs-material/publishing-your-site/#with-github-actions
-  docs-deploy:
-    name: Deploy Docs version
-    runs-on: ubuntu-latest
-    if: github.event_name != 'pull_request' || github.event.action != 'closed'
-    needs: [tests, markdownlint, markdown-link-check]
-    steps:
-      - name: Check out code
-        uses: actions/checkout@v4
-        with:
-          # checkout all commits to get accurate page revision times
-          # for the git-revision-date-localized plugin
-          fetch-depth: '0'
-
-      - name: Setup Python
-        uses: actions/setup-python@v5
-        with:
-          python-version: "3.10"
-          cache: pip
-          cache-dependency-path: '**/requirements/docs.txt'
-
-      - name: Download HTML coverage report
-        uses: actions/download-artifact@v4
-        with:
-          name: htmlcov
-          path: htmlcov
-
-      - name: Install dependencies
-        run: python -m pip install -r requirements/docs.txt
-
-      - name: "Deploy pr-${{ github.event.number }} version of the Docs"
-        if: github.event_name == 'pull_request'
-        run: |
-          git config user.name ci-bot
-          git config user.email ci-bot@example.com
-          mike deploy --push pr-${{ github.event.number }}
-
-      - name: Deploy main version of the Docs
-        if: github.event_name == 'push' && github.ref == 'refs/heads/main'
-        run: |
-          git config user.name ci-bot
-          git config user.email ci-bot@example.com
-          mike deploy --push --update-aliases main latest
-
-  docs-delete:
-    name: Delete Docs version
-    runs-on: ubuntu-latest
-    if: github.event_name == 'pull_request' && github.event.action == 'closed'
-    steps:
-      - name: Check out code
-        uses: actions/checkout@v4
-        with:
-          # checkout all commits and branches to get gh-pages
-          fetch-depth: '0'
-
-      - name: Setup Python
-        uses: actions/setup-python@v5
-        with:
-          python-version: "3.10"
-          cache: pip
-          cache-dependency-path: '**/requirements/docs.txt'
-
-      - name: Install dependencies
-        run: python -m pip install -r requirements/docs.txt
-
-      - name: "Delete pr-${{ github.event.number }} version of the Docs"
-        run: |
-          git config user.name ci-bot
-          git config user.email ci-bot@example.com
-          mike delete --push pr-${{ github.event.number }}
-
-
-

Release Workflow

-

TBD.

-
-Release workflow source code -

.github/workflows/release.yml

-
# this file is *not* meant to cover or endorse the use of GitHub Actions, but rather to
-# help make automated releases for this project
-
-name: Release
-
-on:
-  release:
-    types: [published]
-
-jobs:
-  build-and-publish:
-    runs-on: ubuntu-latest
-    steps:
-    - name: Checkout
-      uses: actions/checkout@v4
-    - name: Set up Python
-      uses: actions/setup-python@v5
-      with:
-        python-version: '3.x'
-    - name: Install build dependencies
-      run: python -m pip install -U setuptools wheel build
-    - name: Build
-      run: python -m build .
-    - name: Publish
-      uses: pypa/gh-action-pypi-publish@master
-      with:
-        password: ${{ secrets.PYPI_API_TOKEN }}
-
-
- -
-
- - - Last update: - March 21, 2023 - - - -
- - - - - - -
-
- - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pr-24/developer_guide/releases/index.html b/pr-24/developer_guide/releases/index.html deleted file mode 100644 index c8470a8..0000000 --- a/pr-24/developer_guide/releases/index.html +++ /dev/null @@ -1,992 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - Releases - Xlbudget Docs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - - -
- - - - - - - -
- -
- - - - -
-
- - - -
-
-
- - - - - - -
-
-
- - - -
-
-
- - - -
-
-
- - - -
-
- - - - - - - - - - - - - - - - - - - - -

Releases

-

Preparing for your first release

-
    -
  1. Go through pyproject.toml and update the relevant fields
  2. -
  3. In tox.ini, set check-release as a default environment by adding it to envlist
  4. -
  5. In this file, uncomment badges and uncomment and update their URLs with the package name - where applicable
  6. -
  7. Create a repository secret named PYPI_API_TOKEN with the corresponding value
  8. -
-

Publishing a new release

-

TBD.

- -
-
- - - Last update: - March 19, 2023 - - - -
- - - - - - -
-
- - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pr-24/developer_guide/vscode/index.html b/pr-24/developer_guide/vscode/index.html deleted file mode 100644 index 3bd04f3..0000000 --- a/pr-24/developer_guide/vscode/index.html +++ /dev/null @@ -1,1180 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - VS Code - Xlbudget Docs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - - -
- - - - - - - -
- -
- - - - -
-
- - - -
-
-
- - - - - - -
-
-
- - - -
-
-
- - - -
-
-
- - - -
-
- - - - - - - - - - - - - - - - - - - - -

VS Code

-

Setup for VS Code

-
    -
  1. cd to the root directory of the repository
  2. -
  3. Create the dev development environment: - tox devenv -e dev .venv
  4. -
  5. Open the repository in VS Code: code .
  6. -
  7. Install the recommended extensions
  8. -
-

Configuration Files

-

VS Code configuration files can be found in the .vscode/ directory.

-
    -
  • cspell.json: configuration for the spell checker
  • -
  • extensions.json: recommended extensions
      -
    • See the recommended extensions by searching for "@recommended" in the Extensions view
    • -
    -
  • -
  • google_docstring_custom_template.mustache: a custom docstrings template for - autoDocstring - until this issue is resolved
  • -
  • launch.json: launch configurations
      -
    • Run the launch configurations from the Run and Debug view
    • -
    -
  • -
  • settings.json: settings
  • -
-

Shortcuts

-

Useful VS Code shortcuts that aren't specific to this repository.

-
-

MacOS Shortcuts

-

For keyboard shortcuts on MacOS, substitute Ctrl with Cmd.

-
-
    -
  • Open Quick Open: Ctrl+P
      -
    • Search for files
    • -
    • Open the command palette by typing "> "
    • -
    • Search for tasks by typing "task "
    • -
    • Search for launch configurations by typing "debug "
    • -
    -
  • -
  • Open Command Palette: Ctrl+Shift+P
  • -
-

Integrations

-

Docs

-

Related recommended extensions enhance Markdown file previews, check for markdownlint errors, -enhance VS Code Markdown support, add autocomplete for mkdocs.yml, and more:

-
    -
  • The enhanced Markdown file preview replaces VS Code's built-in preview
  • -
  • The configuration file for markdownlint is .markdownlint.json
  • -
  • Format tables in a Markdown file with Alt+Shift+F
      -
    • On Linux, the shortcut is Ctrl+Shift+I
    • -
    -
  • -
-

There are also launch configurations to run the development server and open browsers:

- - - - - - - - - - - - - - - - - - - - - -
Launch ConfigurationDescription
Serve Docsruns mkdocs serve
Open Docs in Chromeruns mkdocs serve and open Docs in Chrome
Open Docs in Edgeruns mkdocs serve and open Docs in Microsoft Edge
-

Python

-

Related recommended extensions improve autocomplete, format on save, lint, test, and more:

-
    -
  • Adds autocomplete for docstrings, type hints, and functions
  • -
  • Runs the black and isort - formatters when a file is saved
  • -
  • Runs the flake8 and mypy - linters when a file is saved and after formatters run
  • -
  • Run tests from the Testing view
  • -
-

Common Extensions & Settings

-

Other recommended extensions further improve autocomplete (AI, file paths), check spelling, -and show the commit and author who last modified the current line, and more:

-
    -
  • Mark a word as spelled correct by hovering over it, selecting Quick Fix..., - then selecting Add "<word>" to config: .vscode/cspell.json
  • -
  • Hover over the current line blame annoation at the end of the line for more details
  • -
-

Common settings:

-
    -
  • Make whitespace at the end of the line visible
  • -
  • Ruler at the line length limit
  • -
- -
-
- - - Last update: - March 19, 2023 - - - -
- - - - - - -
-
- - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pr-24/gen_ref_pages.py b/pr-24/gen_ref_pages.py deleted file mode 100644 index ebc17be..0000000 --- a/pr-24/gen_ref_pages.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Generate the code reference pages and navigation.""" - -from pathlib import Path - -import mkdocs_gen_files - -nav = mkdocs_gen_files.Nav() - -for path in sorted(Path("src").rglob("*.py")): - module_path = path.relative_to("src").with_suffix("") - doc_path = path.relative_to("src").with_suffix(".md") - full_doc_path = Path("reference", doc_path) - - parts = tuple(module_path.parts) - - if parts[-1] == "__init__": - parts = parts[:-1] - doc_path = doc_path.with_name("index.md") - full_doc_path = full_doc_path.with_name("index.md") - elif parts[-1] == "__main__": - continue - - nav[parts] = doc_path.as_posix() - - with mkdocs_gen_files.open(full_doc_path, "w") as fd: - identifier = ".".join(parts) - fd.write(f"::: {identifier}") - - mkdocs_gen_files.set_edit_path(full_doc_path, Path("../") / path) - -with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file: - nav_file.writelines(nav.build_literate_nav()) diff --git a/pr-24/index.html b/pr-24/index.html deleted file mode 100644 index f51c930..0000000 --- a/pr-24/index.html +++ /dev/null @@ -1,920 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - README - Xlbudget Docs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - - -
- - - - - - - -
- -
- - - - -
-
- - - -
-
-
- - - - - - -
-
-
- - - -
-
-
- - - -
-
-
- - - -
-
- - - - - - - - - - - - - - - - - - - - -

Excel Budget

- -

Test -Coverage

- -

GitHub

- - - -

Xlbudget is a personal bookkeeping assistant that is in active development.

- - - - - - -
-
- - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pr-24/javascripts/mathjax.js b/pr-24/javascripts/mathjax.js deleted file mode 100644 index 080801e..0000000 --- a/pr-24/javascripts/mathjax.js +++ /dev/null @@ -1,16 +0,0 @@ -window.MathJax = { - tex: { - inlineMath: [["\\(", "\\)"]], - displayMath: [["\\[", "\\]"]], - processEscapes: true, - processEnvironments: true - }, - options: { - ignoreHtmlClass: ".*|", - processHtmlClass: "arithmatex" - } -}; - -document$.subscribe(() => { - MathJax.typesetPromise() -}) diff --git a/pr-24/license/index.html b/pr-24/license/index.html deleted file mode 100644 index ab951ad..0000000 --- a/pr-24/license/index.html +++ /dev/null @@ -1,948 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - License - Xlbudget Docs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - - -
- - - - - - - -
- -
- - - - -
-
- - - -
-
-
- - - - - - -
-
-
- - - -
-
-
- - - -
-
-
- - - -
-
- - - - - - - - - - - - - - - - - - - - -

License

-
Copyright (c) 2016 The Python Packaging Authority (PyPA)
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in
-the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-of the Software, and to permit persons to whom the Software is furnished to do
-so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-
- - - - - - -
-
- - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pr-24/objects.inv b/pr-24/objects.inv deleted file mode 100644 index 9d609ad4dc55ae49ca3aae13c8f3416f831f0a6b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 482 zcmV<80UiD$AX9K?X>NERX>N99Zgg*Qc_4OWa&u{KZXhxWBOp+6Z)#;@bUGkdY+`j} zXJvFCL~mnr3L_v^WpZMd?av*PJAarPHb0B7E zY-J#6b0A}HZE$jBb8}^6Aa!$TZf78RY-wUH3V7PJl)-MpAPk1@dx}Wgb*XmUb<(EY zcG%9A15SdL0K%ZQZ$Cm4DTlU67ka~hKWzWThpq~;Az${WbYYLLbO2u2yS_%;bapKPWY0ZCsDO z{2>21nD5Etg{Gf*USZCFS$I`%0EO_drwXj$sJt_Rn3ZmptFZnJA>v0c6-7MV?} - Click here to go to latest. - -{% endblock %} diff --git a/pr-24/reference/SUMMARY/index.html b/pr-24/reference/SUMMARY/index.html deleted file mode 100644 index b4aac1a..0000000 --- a/pr-24/reference/SUMMARY/index.html +++ /dev/null @@ -1,859 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - SUMMARY - Xlbudget Docs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
- -
- - - - - - - - -
- - - - - - - -
- -
- - - - -
-
- - - -
-
-
- - - - - - -
-
-
- - - -
-
-
- - - -
-
-
- - - -
- -
- - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pr-24/reference/xlbudget/commands/index.html b/pr-24/reference/xlbudget/commands/index.html deleted file mode 100644 index add8d05..0000000 --- a/pr-24/reference/xlbudget/commands/index.html +++ /dev/null @@ -1,2776 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - commands - Xlbudget Docs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - - -
- - - - - - - -
- -
- - - - -
-
- - - -
-
-
- - - - - - -
-
-
- - - -
-
-
- - - -
-
-
- - - -
-
- - - - - - - - - - - - - - - - - - - - -

commands

- -
- - - -
- -

The commands, implemented as implementations of the abstract class Command.

- - - -
- - - - - - -

Classes

- -
- - - -

- Command - - -

- - -
-

- Bases: ABC

- - -

The abstract class that the command implementations implement.

- -

Class Attributes

- - - - - - - - - - - - - - - -
NameTypeDescription
default_path - str -

The default path of the xlbudget file.

- -

Attributes:

- - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
trial - bool -

If True, the xlbudget file will not be written to.

path - str -

The path to the xlbudget file.

- - -
- Source code in src/xlbudget/commands.py -
class Command(ABC):
-    """The abstract class that the command implementations implement.
-
-    Attributes: Class Attributes
-        default_path (str): The default path of the xlbudget file.
-
-    Attributes:
-        trial (bool): If True, the xlbudget file will not be written to.
-        path (str): The path to the xlbudget file.
-    """
-
-    default_path: str = "xlbudget.xlsx"
-
-    @property
-    @abstractmethod
-    def name(self) -> str:
-        """Ensures that the `name` class attribute is defined in subclasses.
-        Part 1/2 of the abstract attribute implementation of `name`.
-        Reference: https://stackoverflow.com/a/53417582.
-        """
-        raise NotImplementedError
-
-    def get_name(self) -> str:
-        """Used to access the `name` class attribute defined in subclasses.
-        Part 2/2 of the abstract attribute implementation of `name`.
-        Reference: https://stackoverflow.com/a/53417582.
-        """
-        return self.name
-
-    @property
-    @abstractmethod
-    def aliases(self) -> List[str]:
-        """Ensures that the `aliases` class attribute is defined in subclasses.
-        Part 1/2 of the abstract attribute implementation of `aliases`.
-        Reference: https://stackoverflow.com/a/53417582.
-        """
-        raise NotImplementedError
-
-    def get_aliases(self) -> List[str]:
-        """Used to access the `aliases` class attribute defined in subclasses.
-        Part 2/2 of the abstract attribute implementation of `aliases`.
-        Reference: https://stackoverflow.com/a/53417582.
-        """
-        return self.aliases
-
-    @classmethod
-    def configure_common_args(cls, parser: ArgumentParser) -> None:
-        """Configures the arguments that are used by all commands.
-
-        Args:
-            parser (ArgumentParser): The argument parser.
-        """
-        parser.add_argument(
-            "-t",
-            "--trial",
-            action="store_true",
-            help="try a command without generating/updating the xlbudget file",
-        )
-        parser.add_argument(
-            "-p",
-            "--path",
-            help="path to the xlbudget file (default: %(default)s)",
-            default=cls.default_path,
-        )
-
-    @classmethod
-    @abstractmethod
-    def configure_args(cls, subparsers: _SubParsersAction) -> None:
-        pass
-
-    @abstractmethod
-    def __init__(self, args: Namespace) -> None:
-        self.trial = args.trial
-
-        self._check_path(args.path)
-        self.path = args.path
-
-    @staticmethod
-    def _check_path(path: str) -> None:
-        """Check that `path` is a valid path to an xlbudget file.
-
-        Args:
-            path (str): The xlbudget path.
-
-        Raises:
-            ValueError: If `path` is not a XLSX file.
-            FileNotFoundError: If `path` is not in an existing directory.
-        """
-        xlsx_ext = ".xlsx"
-        if not path.endswith(xlsx_ext):
-            raise ValueError(f"Path '{path}' does not end with '{xlsx_ext}'")
-
-        dir = os.path.dirname(path)
-        if dir and not os.path.isdir(dir):
-            raise FileNotFoundError(f"Directory '{dir}' does not exist")
-
-    @abstractmethod
-    def run(self) -> None:
-        pass
-
-
- - - -
- - - - - -

Attributes

- -
- - - -
- aliases - - - - property - abstractmethod - - -
-
aliases: List[str]
-
- -
- -

Ensures that the aliases class attribute is defined in subclasses. -Part ½ of the abstract attribute implementation of aliases. -Reference: https://stackoverflow.com/a/53417582.

-
- -
- -
- - - -
- name - - - - property - abstractmethod - - -
-
name: str
-
- -
- -

Ensures that the name class attribute is defined in subclasses. -Part ½ of the abstract attribute implementation of name. -Reference: https://stackoverflow.com/a/53417582.

-
- -
- -

Functions

- -
- - - -
- _check_path - - - - staticmethod - - -
-
_check_path(path)
-
- -
- -

Check that path is a valid path to an xlbudget file.

- -

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
path - str -

The xlbudget path.

- required -
- -

Raises:

- - - - - - - - - - - - - - - - - -
TypeDescription
- ValueError -

If path is not a XLSX file.

- FileNotFoundError -

If path is not in an existing directory.

- -
- Source code in src/xlbudget/commands.py -
@staticmethod
-def _check_path(path: str) -> None:
-    """Check that `path` is a valid path to an xlbudget file.
-
-    Args:
-        path (str): The xlbudget path.
-
-    Raises:
-        ValueError: If `path` is not a XLSX file.
-        FileNotFoundError: If `path` is not in an existing directory.
-    """
-    xlsx_ext = ".xlsx"
-    if not path.endswith(xlsx_ext):
-        raise ValueError(f"Path '{path}' does not end with '{xlsx_ext}'")
-
-    dir = os.path.dirname(path)
-    if dir and not os.path.isdir(dir):
-        raise FileNotFoundError(f"Directory '{dir}' does not exist")
-
-
-
- -
- -
- - - -
- configure_common_args - - - - classmethod - - -
-
configure_common_args(parser)
-
- -
- -

Configures the arguments that are used by all commands.

- -

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
parser - ArgumentParser -

The argument parser.

- required -
- -
- Source code in src/xlbudget/commands.py -
63
-64
-65
-66
-67
-68
-69
-70
-71
-72
-73
-74
-75
-76
-77
-78
-79
-80
-81
@classmethod
-def configure_common_args(cls, parser: ArgumentParser) -> None:
-    """Configures the arguments that are used by all commands.
-
-    Args:
-        parser (ArgumentParser): The argument parser.
-    """
-    parser.add_argument(
-        "-t",
-        "--trial",
-        action="store_true",
-        help="try a command without generating/updating the xlbudget file",
-    )
-    parser.add_argument(
-        "-p",
-        "--path",
-        help="path to the xlbudget file (default: %(default)s)",
-        default=cls.default_path,
-    )
-
-
-
- -
- -
- - - -
- get_aliases - - -
-
get_aliases()
-
- -
- -

Used to access the aliases class attribute defined in subclasses. -Part 2/2 of the abstract attribute implementation of aliases. -Reference: https://stackoverflow.com/a/53417582.

- -
- Source code in src/xlbudget/commands.py -
56
-57
-58
-59
-60
-61
def get_aliases(self) -> List[str]:
-    """Used to access the `aliases` class attribute defined in subclasses.
-    Part 2/2 of the abstract attribute implementation of `aliases`.
-    Reference: https://stackoverflow.com/a/53417582.
-    """
-    return self.aliases
-
-
-
- -
- -
- - - -
- get_name - - -
-
get_name()
-
- -
- -

Used to access the name class attribute defined in subclasses. -Part 2/2 of the abstract attribute implementation of name. -Reference: https://stackoverflow.com/a/53417582.

- -
- Source code in src/xlbudget/commands.py -
40
-41
-42
-43
-44
-45
def get_name(self) -> str:
-    """Used to access the `name` class attribute defined in subclasses.
-    Part 2/2 of the abstract attribute implementation of `name`.
-    Reference: https://stackoverflow.com/a/53417582.
-    """
-    return self.name
-
-
-
- -
- - - -
- -
- -
- -
- - - -

- Update - - -

- - -
-

- Bases: Command

- - -

The update command updates an existing xlbudget file.

- -

Class Attributes

- - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
name - str -

The command's CLI name.

aliases - List[str] -

The command's CLI aliases.

- -

Attributes:

- - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
input - Optional[str] -

The path to the input file, otherwise paste in terminal.

format - inputformat.InputFormat -

The input file format.

year - Optional[str] -

The year all transactions were made, only relevant if -the input format is 'BMO_CC_ADOBE'.

- - -
- Source code in src/xlbudget/commands.py -
class Update(Command):
-    """The `update` command updates an existing xlbudget file.
-
-    Attributes: Class Attributes
-        name (str): The command's CLI name.
-        aliases (List[str]): The command's CLI aliases.
-
-    Attributes:
-        input (Optional[str]): The path to the input file, otherwise paste in terminal.
-        format (inputformat.InputFormat): The input file format.
-        year (Optional[str]): The year all transactions were made, only relevant if
-            the input format is 'BMO_CC_ADOBE'.
-    """
-
-    name: str = "update"
-    aliases: List[str] = ["u"]
-
-    @classmethod
-    def configure_args(cls, subparsers: _SubParsersAction) -> None:
-        """Configures the argument parser for the `update` command.
-
-        Args:
-            subparsers (_SubParsersAction): The command `subparsers`.
-        """
-        parser = _add_parser(
-            subparsers,
-            name=cls.name,
-            aliases=cls.aliases,
-            help="update an existing xlbudget file",
-            cmd_cls=Update,
-        )
-
-        # required arguments
-        parser.add_argument(
-            "format",
-            action=GetInputFormats,
-            choices=GetInputFormats.input_formats.keys(),
-            help="select an input format",
-        )
-
-        # optional arguments
-        parser.add_argument("-i", "--input", help="path to the input file")
-        parser.add_argument(
-            "-y",
-            "--year",
-            help="year that all transactions were made, only relevant if input format "
-            "is 'BMO_CC_ADOBE'",
-        )
-
-    def __init__(self, args: Namespace) -> None:
-        super().__init__(args)
-
-        self._check_input(args.input, args.format, args.year)
-        self.input = args.input
-        self.format = args.format
-        self.year = args.year
-
-        logger.debug(f"instance variables: {vars(self)}")
-
-    @staticmethod
-    def _check_input(
-        input: Optional[str], input_format: Optional[InputFormat], year: Optional[str]
-    ) -> None:
-        """Check that `input` and `year` are valid.
-
-        Args:
-            input (Optional[str]): The input path.
-            input_format (Optional[InputFormat]): The input format.
-            year (Optional[str]): The year of all transactions.
-
-        Raises:
-            ValueError: If `input` is not None and the wrong file extension or DNE.
-            ValueError: If `year` is None when `input_format` is 'BMO_CC_ADOBE'.
-        """
-        if input is None:
-            return
-
-        in_ext = (".csv", ".tsv", ".txt")
-        if not input.endswith(in_ext):
-            raise ValueError(f"Input '{input}' does not end with one of '{in_ext}'")
-
-        if not os.path.isfile(input):
-            raise ValueError(f"Input '{input}' is not an existing file")
-
-        # get key from value: https://stackoverflow.com/a/13149770
-        if input_format is not None:
-            # validate year
-            format = list(GetInputFormats.input_formats.keys())[
-                list(GetInputFormats.input_formats.values()).index(input_format)
-            ]
-            if format == "BMO_CC_ADOBE" and year is None:
-                raise ValueError(f"Must specify 'year' argument when {format=}")
-
-            # validate input file type in more detail
-            if input.endswith(".csv") and not input_format.seperator == ",":
-                raise ValueError(f"Input file should be CSV for {format=}")
-
-            if input.endswith(".tsv") and not input_format.seperator == "\t":
-                raise ValueError(f"Input file should be TSV for {format=}")
-
-    def run(self) -> None:
-        logger.info(f"Parsing input {self.input}")
-        df = parse_input(self.input, self.format, self.year)
-        logger.debug(f"input file: {df.shape=}, df.dtypes=\n{df.dtypes}")
-        logger.debug(f"df.head()=\n{df.head()}")
-
-        if os.path.exists(self.path):
-            logger.info(f"Loading xlbudget file {self.path}")
-            wb = load_workbook(self.path)
-        else:
-            logger.warning(f"xlbudget file {self.path} does not exist, creating")
-            wb = Workbook()
-            ws = wb.active
-            # ignore type mismatch of active worksheet
-            wb.remove(ws)  # type: ignore[arg-type]
-
-        logger.info("Updating xlbudget file")
-        update_xlbudget(wb, df)
-
-        if not self.trial:
-            logger.info(f"Saving xlbudget file to {self.path}")
-            wb.save(self.path)
-        else:
-            logger.info(f"Trial run: not saving xlbudget file to {self.path}")
-
-
- - - -
- - - - - - - -

Functions

- -
- - - -
- _check_input - - - - staticmethod - - -
-
_check_input(input, input_format, year)
-
- -
- -

Check that input and year are valid.

- -

Parameters:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
input - Optional[str] -

The input path.

- required -
input_format - Optional[InputFormat] -

The input format.

- required -
year - Optional[str] -

The year of all transactions.

- required -
- -

Raises:

- - - - - - - - - - - - - - - - - -
TypeDescription
- ValueError -

If input is not None and the wrong file extension or DNE.

- ValueError -

If year is None when input_format is 'BMO_CC_ADOBE'.

- -
- Source code in src/xlbudget/commands.py -
@staticmethod
-def _check_input(
-    input: Optional[str], input_format: Optional[InputFormat], year: Optional[str]
-) -> None:
-    """Check that `input` and `year` are valid.
-
-    Args:
-        input (Optional[str]): The input path.
-        input_format (Optional[InputFormat]): The input format.
-        year (Optional[str]): The year of all transactions.
-
-    Raises:
-        ValueError: If `input` is not None and the wrong file extension or DNE.
-        ValueError: If `year` is None when `input_format` is 'BMO_CC_ADOBE'.
-    """
-    if input is None:
-        return
-
-    in_ext = (".csv", ".tsv", ".txt")
-    if not input.endswith(in_ext):
-        raise ValueError(f"Input '{input}' does not end with one of '{in_ext}'")
-
-    if not os.path.isfile(input):
-        raise ValueError(f"Input '{input}' is not an existing file")
-
-    # get key from value: https://stackoverflow.com/a/13149770
-    if input_format is not None:
-        # validate year
-        format = list(GetInputFormats.input_formats.keys())[
-            list(GetInputFormats.input_formats.values()).index(input_format)
-        ]
-        if format == "BMO_CC_ADOBE" and year is None:
-            raise ValueError(f"Must specify 'year' argument when {format=}")
-
-        # validate input file type in more detail
-        if input.endswith(".csv") and not input_format.seperator == ",":
-            raise ValueError(f"Input file should be CSV for {format=}")
-
-        if input.endswith(".tsv") and not input_format.seperator == "\t":
-            raise ValueError(f"Input file should be TSV for {format=}")
-
-
-
- -
- -
- - - -
- configure_args - - - - classmethod - - -
-
configure_args(subparsers)
-
- -
- -

Configures the argument parser for the update command.

- -

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
subparsers - _SubParsersAction -

The command subparsers.

- required -
- -
- Source code in src/xlbudget/commands.py -
@classmethod
-def configure_args(cls, subparsers: _SubParsersAction) -> None:
-    """Configures the argument parser for the `update` command.
-
-    Args:
-        subparsers (_SubParsersAction): The command `subparsers`.
-    """
-    parser = _add_parser(
-        subparsers,
-        name=cls.name,
-        aliases=cls.aliases,
-        help="update an existing xlbudget file",
-        cmd_cls=Update,
-    )
-
-    # required arguments
-    parser.add_argument(
-        "format",
-        action=GetInputFormats,
-        choices=GetInputFormats.input_formats.keys(),
-        help="select an input format",
-    )
-
-    # optional arguments
-    parser.add_argument("-i", "--input", help="path to the input file")
-    parser.add_argument(
-        "-y",
-        "--year",
-        help="year that all transactions were made, only relevant if input format "
-        "is 'BMO_CC_ADOBE'",
-    )
-
-
-
- -
- - - -
- -
- -
-

Functions

- -
- - - -

- _add_parser - - -

-
_add_parser(subparsers, name, aliases, help, cmd_cls)
-
- -
- -

Adds an argument parser for a command. Any configuration that is common -across commands should go here.

- -

Parameters:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
subparsers - _SubParsersAction -

The subparsers object.

- required -
name - str -

The command name.

- required -
aliases - List[str] -

The command aliases.

- required -
help - str -

The command help message.

- required -
cmd_cls - Type[Command] -

The command class.

- required -
- -

Returns:

- - - - - - - - - - - - - -
TypeDescription
- ArgumentParser -

A[n] ArgumentParser for a command.

- -
- Source code in src/xlbudget/commands.py -
def _add_parser(
-    subparsers: _SubParsersAction,
-    name: str,
-    aliases: List[str],
-    help: str,
-    cmd_cls: Type[Command],
-) -> ArgumentParser:
-    """Adds an argument parser for a command. Any configuration that is common
-    across commands should go here.
-
-    Args:
-        subparsers (_SubParsersAction): The subparsers object.
-        name (str): The command name.
-        aliases (List[str]): The command aliases.
-        help (str): The command help message.
-        cmd_cls (Type[Command]): The command class.
-
-    Returns:
-        A[n] `ArgumentParser` for a command.
-    """
-    parser = subparsers.add_parser(name, aliases=aliases, help=help)
-
-    # initialize the command with args.init(...)
-    parser.set_defaults(init=cmd_cls)
-
-    return parser
-
-
-
- -
- -
- - - -

- get_command_classes - - -

-
get_command_classes()
-
- -
- -

Gets all classes that implement the Command abstract class.

- -

Returns:

- - - - - - - - - - - - - -
TypeDescription
- List[Type[Command]] -

A[n] List[Type[Command]] of all command classes.

- -
- Source code in src/xlbudget/commands.py -
def get_command_classes() -> List[Type[Command]]:
-    """Gets all classes that implement the `Command` abstract class.
-
-    Returns:
-        A[n] `List[Type[Command]]` of all command classes.
-    """
-    command_module = sys.modules[__name__]
-    return [getattr(command_module, c.__name__) for c in Command.__subclasses__()]
-
-
-
- -
- - - -
- -
- -
- - - - - - -
-
- - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pr-24/reference/xlbudget/configure/index.html b/pr-24/reference/xlbudget/configure/index.html deleted file mode 100644 index c229c46..0000000 --- a/pr-24/reference/xlbudget/configure/index.html +++ /dev/null @@ -1,1420 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - configure - Xlbudget Docs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - - -
- - - - - - - -
- -
- - - - -
-
- - - -
-
-
- - - - - - -
-
-
- - - -
-
-
- - - -
-
-
- - - -
-
- - - - - - - - - - - - - - - - - - - - -

configure

- -
- - - -
- -

The setup and configuration for xlbudget.

- -
- Logger usage in this file -

The logger can only be used after _configure_logger is called in setup.

-
- - -
- - - - - - -

Classes

-

Functions

- -
- - - -

- _configure_argument_parser - - -

-
_configure_argument_parser()
-
- -
- -

Configures the argument parser for all arguments.

- -

Returns:

- - - - - - - - - - - - - -
TypeDescription
- ArgumentParser -

A[n] ArgumentParser configured for this package.

- -
- Source code in src/xlbudget/configure.py -
31
-32
-33
-34
-35
-36
-37
-38
-39
-40
-41
-42
-43
-44
-45
-46
-47
-48
-49
-50
def _configure_argument_parser() -> ArgumentParser:
-    """Configures the argument parser for all arguments.
-
-    Returns:
-        A[n] `ArgumentParser` configured for this package.
-    """
-    parser = ArgumentParser()
-
-    Command.configure_common_args(parser)
-    _configure_logger_args(parser)
-
-    cmd_subparsers = parser.add_subparsers(
-        title="command",
-        required=True,
-        description="The xlbudget command to run.",
-    )
-    for cmd_cls in get_command_classes():
-        cmd_cls.configure_args(cmd_subparsers)
-
-    return parser
-
-
-
- -
- -
- - - -

- _configure_logger - - -

-
_configure_logger(level)
-
- -
- -

Configures the logger.

-

Since this configuration is global, there is no need to return the logger. -To use the logger in a file, add logger = logging.getLogger(__name__) at the top.

- -

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
level - int -

The logging level.

- required -
- -
- Source code in src/xlbudget/configure.py -
85
-86
-87
-88
-89
-90
-91
-92
-93
-94
-95
-96
-97
def _configure_logger(level: int) -> None:
-    """Configures the logger.
-
-    Since this configuration is global, there is no need to return the logger.
-    To use the logger in a file, add `logger = logging.getLogger(__name__)` at the top.
-
-    Args:
-        level (int): The [logging level](https://docs.python.org/3/library/logging.html#logging-levels).
-    """  # noqa
-    logging.basicConfig(
-        level=level,
-        format="%(levelname)s - %(name)s:%(lineno)s - %(message)s",
-    )
-
-
-
- -
- -
- - - -

- _configure_logger_args - - -

-
_configure_logger_args(parser)
-
- -
- -

Configures the argument parser for logger arguments. -The log level configuration was adapted from -this Stack Overflow answer.

- -

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
parser - ArgumentParser -

The argument parser to update.

- required -
- -
- Source code in src/xlbudget/configure.py -
53
-54
-55
-56
-57
-58
-59
-60
-61
-62
-63
-64
-65
-66
-67
-68
-69
-70
-71
-72
-73
-74
-75
-76
-77
-78
-79
-80
-81
-82
def _configure_logger_args(parser: ArgumentParser) -> None:
-    """Configures the argument parser for logger arguments.
-    The log level configuration was adapted from
-    [this Stack Overflow answer](https://stackoverflow.com/a/20663028).
-
-    Args:
-        parser (ArgumentParser): The argument parser to update.
-    """
-    group_log = parser.add_argument_group(
-        "logger configuration",
-        description="Arguments that override the default logger configuration.",
-    )
-    group_log_lvl = group_log.add_mutually_exclusive_group()
-    group_log_lvl.add_argument(
-        "-d",
-        "--debug",
-        help="print lots of debugging statements; can't use with -v/--verbose",
-        action="store_const",
-        dest="log_level",
-        const=logging.DEBUG,
-        default=logging.WARNING,
-    )
-    group_log_lvl.add_argument(
-        "-v",
-        "--verbose",
-        help="be verbose; can't use with -d/--debug",
-        action="store_const",
-        dest="log_level",
-        const=logging.INFO,
-    )
-
-
-
- -
- -
- - - -

- setup - - -

-
setup()
-
- -
- -

Package-level setup and configuration.

- -

Returns:

- - - - - - - - - - - - - -
TypeDescription
- Namespace -

A[n] Namespace containing the parsed CLI arguments.

- -
- Source code in src/xlbudget/configure.py -
14
-15
-16
-17
-18
-19
-20
-21
-22
-23
-24
-25
-26
-27
-28
def setup() -> Namespace:
-    """Package-level setup and configuration.
-
-    Returns:
-        A[n] `Namespace` containing the parsed CLI arguments.
-    """
-    parser = _configure_argument_parser()
-    args = parser.parse_args()
-    _configure_logger(args.log_level)
-
-    # log args after call to _configure_logger
-    logger = logging.getLogger(__name__)
-    logger.debug(f"parsed CLI arguments: {args}")
-
-    return args
-
-
-
- -
- - - -
- -
- -
- - - - - - -
-
- - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pr-24/reference/xlbudget/index.html b/pr-24/reference/xlbudget/index.html deleted file mode 100644 index e157e86..0000000 --- a/pr-24/reference/xlbudget/index.html +++ /dev/null @@ -1,1007 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - xlbudget - Xlbudget Docs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - - -
- - - - - - - -
- -
- - - - -
-
- - - -
-
-
- - - - - - -
-
-
- - - -
-
-
- - - -
-
-
- - - -
-
- - - - - - - - - - - - - - - - - - - - -

xlbudget

- -
- - - -
- -

Xlbudget: a personal bookkeeping assistant.

- - - -
- - - - - - - -

Functions

- -
- - - -

- main - - -

-
main()
-
- -
- -

Entry point for the application script.

- -
- Source code in src/xlbudget/__init__.py -
 6
- 7
- 8
- 9
-10
def main():
-    "Entry point for the application script."
-    args = setup()
-    cmd = args.init(args)
-    cmd.run()
-
-
-
- -
- - - -
- -
- -
- - - - - - -
-
- - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pr-24/reference/xlbudget/inputformat/index.html b/pr-24/reference/xlbudget/inputformat/index.html deleted file mode 100644 index 23b4b2f..0000000 --- a/pr-24/reference/xlbudget/inputformat/index.html +++ /dev/null @@ -1,1963 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - inputformat - Xlbudget Docs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - - -
- - - - - - - -
- -
- - - - -
-
- - - -
-
-
- - - - - - -
-
-
- - - -
-
-
- - - -
-
-
- - - -
-
- - - - - - - - - - - - - - - - - - - - -

inputformat

- -
- - - -
- -

Input file format definitions.

- - - -
- - - - - - -

Classes

- -
- - - -

- GetInputFormats - - -

- - -
-

- Bases: Action

- - -

Argparse action for the format argument. -Adapted from this Stack Overflow answer.

- -

Attributes:

- - - - - - - - - - - - - - - -
NameTypeDescription
input_formats - Dict[str, InputFormat] -

Maps format names to values.

- - -
- Source code in src/xlbudget/inputformat.py -
class GetInputFormats(Action):
-    """Argparse action for the format argument.
-    Adapted from [this Stack Overflow answer](https://stackoverflow.com/a/50799463).
-
-    Attributes:
-        input_formats (Dict[str, InputFormat]): Maps format names to values.
-    """
-
-    input_formats: Dict[str, InputFormat] = {
-        n: globals()[n] for n in globals() if isinstance(globals()[n], InputFormat)
-    }
-
-    def __call__(self, parser, namespace, values, option_string=None):
-        setattr(namespace, self.dest, self.input_formats[values])
-
-
- - - -
- - - - - - - - - - - -
- -
- -
- -
- - - -

- InputFormat - - -

- - -
-

- Bases: NamedTuple

- - -

Specifies the format of the input file.

- -

Attributes:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
header - int -

The 0-indexed row of the header in the input file.

names - List[str] -

The column names.

usecols - List[int] -

The first len(MONTH_COLUMNS) elements are indices of -columns that map to MONTH_COLUMNS, there may indices after for columns -required for post-processing.

ignores - List[str] -

Ignore transactions that contain with these regex patterns.

pre_processing - Callable -

The function to call before pd.read_csv().

post_processing - Callable -

The function to call after pd.read_csv().

sep - str -

The separator.

- - -
- Source code in src/xlbudget/inputformat.py -
18
-19
-20
-21
-22
-23
-24
-25
-26
-27
-28
-29
-30
-31
-32
-33
-34
-35
-36
-37
-38
-39
-40
-41
-42
class InputFormat(NamedTuple):
-    """Specifies the format of the input file.
-
-    Attributes:
-        header (int): The 0-indexed row of the header in the input file.
-        names (List[str]): The column names.
-        usecols (List[int]): The first len(`MONTH_COLUMNS`) elements are indices of
-            columns that map to `MONTH_COLUMNS`, there may indices after for columns
-            required for post-processing.
-        ignores (List[str]): Ignore transactions that contain with these regex patterns.
-        pre_processing (Callable): The function to call before `pd.read_csv()`.
-        post_processing (Callable): The function to call after `pd.read_csv()`.
-        sep (str): The separator.
-    """
-
-    header: int
-    names: List[str]
-    usecols: List[int]
-    ignores: List[str]
-    pre_processing: Callable = lambda input, year: input
-    post_processing: Callable = lambda df: df
-    seperator: str = ","
-
-    def get_usecols_names(self):
-        return [self.names[i] for i in self.usecols[:3]]
-
-
- - - -
- - - - - - - - - - - -
- -
- -
-

Functions

- -
- - - -

- bmo_acct_web_post_processing - - -

-
bmo_acct_web_post_processing(df)
-
- -
- -

Creates the "Amount" column.

- -

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
df - pd.DataFrame -

The dataframe to process.

- required -
- -

Returns:

- - - - - - - - - - - - - -
TypeDescription
- pd.DataFrame -

A[n] pd.DataFrame that combines "Amount" and "Money in" to create "Amount".

- -
- Source code in src/xlbudget/inputformat.py -
def bmo_acct_web_post_processing(df: pd.DataFrame) -> pd.DataFrame:
-    """Creates the "Amount" column.
-
-    Args:
-        df (pd.DataFrame): The dataframe to process.
-
-    Returns:
-        A[n] `pd.DataFrame` that combines "Amount" and "Money in" to create "Amount".
-    """
-    df["Amount"] = df["Amount"].replace("[$,]", "", regex=True).astype(float)
-    df["Money in"] = df["Money in"].replace("[$,]", "", regex=True).astype(float)
-    df["Amount"] = np.where(df["Money in"].isna(), df["Amount"], df["Money in"])
-    df = df.drop("Money in", axis=1)
-    return df
-
-
-
- -
- -
- - - -

- bmo_cc_adobe_pre_processing - - -

-
bmo_cc_adobe_pre_processing(_input, year)
-
- -
- -

Create CSV from input with each element on a new line.

- -

Parameters:

- - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
_input - Optional[str] -

The file to process, if None then read from stdin.

- required -
year - str -

The year of all transactions.

- required -
- -

Returns:

- - - - - - - - - - - - - -
TypeDescription
- io.StringIO -

A[n] io.StringIO CSV.

- -
- Source code in src/xlbudget/inputformat.py -
48
-49
-50
-51
-52
-53
-54
-55
-56
-57
-58
-59
-60
-61
-62
-63
-64
-65
-66
-67
-68
-69
-70
-71
-72
-73
-74
-75
-76
-77
-78
-79
-80
-81
-82
-83
-84
-85
-86
-87
-88
-89
-90
-91
-92
-93
-94
def bmo_cc_adobe_pre_processing(_input: Optional[str], year: str) -> io.StringIO:
-    """Create CSV from input with each element on a new line.
-
-    Args:
-        _input (Optional[str]): The file to process, if `None` then read from stdin.
-        year (str): The year of all transactions.
-
-    Returns:
-        A[n] `io.StringIO` CSV.
-    """
-    # get lines from stdin or file
-    if _input is None:
-        lines = []
-        for line in sys.stdin:
-            lines.append(line.strip())
-    else:
-        with open(_input) as f:
-            lines = f.read().splitlines()
-
-    rows = []
-    i = 0
-    is_header = True
-    while i < len(lines):
-        elems = lines[i : i + 4]
-
-        # reformat dates and add year
-        if not is_header:
-            elems[0] = year + datetime.strptime(elems[0], "%b. %d").strftime("-%m-%d")
-            elems[1] = year + datetime.strptime(elems[1], "%b. %d").strftime("-%m-%d")
-
-        # add negative sign to amounts that are not credited (CR on next line)
-        if i + 4 < len(lines) and lines[i + 4] == "CR":
-            i += 5
-        else:
-            if not is_header:
-                elems[-1] = "-" + elems[-1]
-
-            i += 4
-
-        row = "\t".join(elems) + "\n"
-        rows.append(row)
-
-        if is_header:
-            is_header = False
-
-    new_input = "".join(rows)
-    return io.StringIO(new_input)
-
-
-
- -
- -
- - - -

- bmo_cc_web_post_processing - - -

-
bmo_cc_web_post_processing(df)
-
- -
- -

Formats the "Money in/out" column.

- -

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
df - pd.DataFrame -

The dataframe to process.

- required -
- -

Returns:

- - - - - - - - - - - - - -
TypeDescription
- pd.DataFrame -

A[n] pd.DataFrame that converts "Money in/out" to a float.

- -
- Source code in src/xlbudget/inputformat.py -
def bmo_cc_web_post_processing(df: pd.DataFrame) -> pd.DataFrame:
-    """Formats the "Money in/out" column.
-
-    Args:
-        df (pd.DataFrame): The dataframe to process.
-
-    Returns:
-        A[n] `pd.DataFrame` that converts "Money in/out" to a float.
-    """
-    df["Money in/out"] = (
-        df["Money in/out"].replace("[$,]", "", regex=True).astype(float)
-    )
-    return df
-
-
-
- -
- -
- - - -

- parse_input - - -

-
parse_input(input, format, year)
-
- -
- -

Parses an input.

- -

Parameters:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
input - Optional[str] -

The path to the input file, if None parse from stdin.

- required -
format - InputFormat -

The input format.

- required -
year - Optional[str] -

The year of all transactions.

- required -
- -

Raises:

- - - - - - - - - - - - - -
TypeDescription
- ValueError -

If input contains duplicate transactions.

- -

Returns:

- - - - - - - - - - - - - -
TypeDescription
- pd.DataFrame -

A[n] pd.DataFrame where the columns match the xlbudget file's column names.

- -
- Source code in src/xlbudget/inputformat.py -
def parse_input(
-    input: Optional[str], format: InputFormat, year: Optional[str]
-) -> pd.DataFrame:
-    """Parses an input.
-
-    Args:
-        input (Optional[str]): The path to the input file, if None parse from stdin.
-        format (InputFormat): The input format.
-        year (Optional[str]): The year of all transactions.
-
-    Raises:
-        ValueError: If input contains duplicate transactions.
-
-    Returns:
-        A[n] `pd.DataFrame` where the columns match the xlbudget file's column names.
-    """
-    input_initially_none = input is None
-    if input_initially_none:
-        print("Paste your transactions here (CTRL+D twice on a blank line to end):")
-
-    input = format.pre_processing(input, year)
-
-    df = pd.read_csv(
-        input if input is not None else sys.stdin,
-        sep=format.seperator,
-        index_col=False,
-        names=format.names,
-        header=format.header if input is not None else None,
-        usecols=format.usecols,
-        parse_dates=[0],
-        skip_blank_lines=False,
-    )
-
-    if input_initially_none:
-        print("---End of transactions---")
-
-    df = format.post_processing(df)
-
-    # convert first column to datetime and replace any invalid values with NaT
-    df[df.columns[0]] = pd.to_datetime(df[df.columns[0]], errors="coerce")
-
-    df = df_drop_na(df)
-
-    df.columns = df.columns.str.strip()
-
-    # order columns to match `MONTH_COLUMNS`
-    df = df[format.get_usecols_names()]
-
-    # rename columns to match `MONTH_COLUMNS`
-    df = df.set_axis([c.name for c in MONTH_COLUMNS], axis="columns")
-
-    # sort rows by date
-    df = df.sort_values(by=list(df.columns), ascending=True)
-
-    # strip whitespace from descriptions
-    df["Description"] = df["Description"].str.strip()
-
-    # drop ignored transactions
-    df = df_drop_ignores(df, "|".join(format.ignores))
-
-    # TODO: write issues to make ignoring identical transactions interactive
-    # TODO: investigate autocompletions
-    if df.duplicated().any():
-        logger.warning(
-            "The following transactions are identical:\n"
-            f"{df[df.duplicated(keep=False)]}"
-        )
-
-    return df
-
-
-
- -
- - - -
- -
- -
- - - - - - -
-
- - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pr-24/reference/xlbudget/rwxlb/index.html b/pr-24/reference/xlbudget/rwxlb/index.html deleted file mode 100644 index c6d128f..0000000 --- a/pr-24/reference/xlbudget/rwxlb/index.html +++ /dev/null @@ -1,2055 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - rwxlb - Xlbudget Docs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - - -
- - - - - - - -
- -
- - - - -
-
- - - -
-
-
- - - - - - -
-
-
- - - -
-
-
- - - -
-
-
- - - -
-
- - - - - - - - - - - - - - - - - - - - -

rwxlb

- -
- - - -
- -

xlbudget file reading and writing.

- - - -
- - - - - - -

Classes

- -
- - - -

- TablePosition - - -

- - -
- - -

The state and bounds of a worksheet table. -Read-only fields were implemented with properties that return mangled variables.

- - -
- Source code in src/xlbudget/rwxlb.py -
46
-47
-48
-49
-50
-51
-52
-53
-54
-55
-56
-57
-58
-59
-60
-61
-62
-63
-64
-65
-66
-67
-68
-69
-70
-71
-72
-73
-74
-75
-76
-77
-78
-79
-80
-81
-82
-83
-84
-85
-86
-87
-88
class TablePosition:
-    """The state and bounds of a worksheet table.
-    Read-only fields were implemented with properties that return mangled variables.
-    """
-
-    def __init__(self, ref: str) -> None:
-        # excel ref format: "<top left cell coordinate>:<bottom right cell coordinate>"
-        start, end = ref.split(":")
-
-        self.__first_col, self.__header_row = coordinate_from_string(start)
-        self.next_row = self.__header_row + 1
-        self.__first_col_ind = column_index_from_string(self.__first_col)
-
-        self.__last_col, self.__initial_last_row = coordinate_from_string(end)
-
-    @property
-    def header_row(self) -> int:
-        return self.__header_row
-
-    @property
-    def first_col(self) -> int:
-        return self.__first_col_ind
-
-    @property
-    def initial_last_row(self) -> int:
-        return self.__initial_last_row
-
-    def __repr__(self) -> str:
-        return (
-            f"{self.__class__.__name__}(next_row={self.next_row}, "
-            f"first_col={self.first_col}, initial_last_row={self.initial_last_row}, "
-            f"header_row={self.header_row})"
-        )
-
-    def get_ref(self) -> str:
-        # Excel tables must have at least 2 rows: 1 header and 1+ data. `last_row` is
-        # implemented as follows so that `next_row` can be incremented consistently.
-        last_row = (
-            self.next_row - 1
-            if self.next_row - 1 >= self.header_row + 1
-            else self.header_row + 1
-        )
-        return f"{self.__first_col}{self.header_row}:{self.__last_col}{last_row}"
-
-
- - - -
- - - - - - - - - - - -
- -
- -
-

Functions

- -
- - - -

- create_year_sheet - - -

-
create_year_sheet(wb, year)
-
- -
- -

Creates a year sheet, with a table for each month.

- -

Parameters:

- - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
wb - openpyxl.workbook.workbook.Workbook -

The workbook to create the sheet in.

- required -
year - int -

The year.

- required -
- -

Raises:

- - - - - - - - - - - - - -
TypeDescription
- ValueError -

If year sheet year already exists in the workbook wb.

- -
- Source code in src/xlbudget/rwxlb.py -
def create_year_sheet(wb: Workbook, year: int) -> None:
-    """Creates a year sheet, with a table for each month.
-
-    Args:
-        wb (openpyxl.workbook.workbook.Workbook): The workbook to create the sheet in.
-        year (int): The year.
-
-    Raises:
-        ValueError: If year sheet `year` already exists in the workbook `wb`.
-    """
-    index = 0
-    year_str = str(year)
-    if year_str in wb.sheetnames:
-        raise ValueError(f"Year sheet {year_str} already exists")
-
-    logger.debug(f"Creating sheet {year_str} at {index=}")
-    ws = wb.create_sheet(year_str, index)
-    num_tables = len(MONTH_NAME_0_IND)
-
-    for c_start in range(
-        MONTH_TABLES_COL,
-        (len(MONTH_COLUMNS) + 1) * num_tables + MONTH_TABLES_COL,
-        len(MONTH_COLUMNS) + 1,
-    ):
-        month_ind = (c_start - MONTH_TABLES_COL) // (len(MONTH_COLUMNS) + 1)
-        month = MONTH_NAME_0_IND[month_ind]
-        table_name = _get_month_table_name(month, year_str)
-        logger.debug(f"creating {table_name} table")
-
-        _add_table(
-            ws, table_name, c_start, r_start=MONTH_TABLES_ROW, columns=MONTH_COLUMNS
-        )
-
-    logger.debug("Creating summary table")
-    summ_table_name = _get_summary_table_name(year_str)
-    _add_table(ws, summ_table_name, c_start=1, r_start=1, columns=SUMMARY_COLUMNS)
-    summ_tab = ws.tables[summ_table_name]
-    summ_tab_pos = TablePosition(ref=summ_tab.ref)
-
-    for month in MONTH_NAME_0_IND:
-        month_table_name = _get_month_table_name(month, year_str)
-        table_range = f"{month_table_name}[{MONTH_COLUMNS[-1].name}]"
-
-        # set month cell
-        ws.cell(row=summ_tab_pos.next_row, column=summ_tab_pos.first_col).value = month
-
-        # set incomes cell
-        incomes_cell = ws.cell(
-            row=summ_tab_pos.next_row, column=summ_tab_pos.first_col + 1
-        )
-        incomes_cell.value = f'=SUMIFS({table_range}, {table_range}, ">0")'
-        incomes_cell.number_format = SUMMARY_COLUMNS[1].format
-
-        # set expenses cell
-        expenses_cell = ws.cell(
-            row=summ_tab_pos.next_row, column=summ_tab_pos.first_col + 2
-        )
-        expenses_cell.value = f'=-SUMIFS({table_range}, {table_range}, "<=0")'
-        expenses_cell.number_format = SUMMARY_COLUMNS[2].format
-
-        # set net cell
-        net_cell = ws.cell(row=summ_tab_pos.next_row, column=summ_tab_pos.first_col + 3)
-        net_cell.value = f"={incomes_cell.coordinate}-{expenses_cell.coordinate}"
-        net_cell.number_format = SUMMARY_COLUMNS[3].format
-
-        summ_tab_pos.next_row += 1
-
-    summ_tab.ref = summ_tab_pos.get_ref()
-
-    # compute totals
-    # set month cell
-    ws.cell(row=summ_tab_pos.next_row, column=summ_tab_pos.first_col).value = "Total"
-
-    # set other cells and create charts
-    for i in range(1, len(SUMMARY_COLUMNS)):
-        cell = ws.cell(row=summ_tab_pos.next_row, column=summ_tab_pos.first_col + i)
-        cell.value = f"=SUM({summ_table_name}[{SUMMARY_COLUMNS[i].name}])"
-        cell.number_format = SUMMARY_COLUMNS[i].format
-
-        chart = BarChart()
-        data = Reference(
-            ws,
-            min_col=i + 1,
-            min_row=summ_tab_pos.header_row,
-            max_row=summ_tab_pos.next_row - 1,
-        )
-        cats = Reference(
-            ws,
-            min_col=summ_tab_pos.first_col,
-            min_row=summ_tab_pos.header_row + 1,
-            max_row=summ_tab_pos.next_row - 1,
-        )
-        chart.add_data(data, titles_from_data=True)
-        chart.set_categories(cats)
-        chart.legend = None
-        chart.y_axis.numFmt = FORMAT_NUMBER
-        chart.height = 7.5
-        chart.width = 8.5  # type: ignore[assignment]
-        start_col = MONTH_TABLES_COL + (i - 1) * (len(MONTH_COLUMNS) + 1)
-        anchor = f"{get_column_letter(start_col)}1"
-        ws.add_chart(chart, anchor)
-
-
-
- -
- -
- - - -

- df_drop_duplicates - - -

-
df_drop_duplicates(df)
-
- -
- -

Checks for duplicate rows, dropping them in place if any.

- -

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
df - pd.DataFrame -

The original dataframe.

- required -
- -

Returns:

- - - - - - - - - - - - - -
TypeDescription
- pd.DataFrame -

A[n] pd.DataFrame without any duplicate rows.

- -
- Source code in src/xlbudget/rwxlb.py -
def df_drop_duplicates(df: pd.DataFrame) -> pd.DataFrame:
-    """Checks for duplicate rows, dropping them in place if any.
-
-    Args:
-        df (pd.DataFrame): The original dataframe.
-
-    Returns:
-        A[n] `pd.DataFrame` without any duplicate rows.
-    """
-    duplicated = df.duplicated()
-    duplicates = df[duplicated]
-    if not duplicates.empty:
-        logger.warning(f"Dropping duplicate transactions:\n{duplicates}")
-        return df[~duplicated]
-    return df
-
-
-
- -
- -
- - - -

- df_drop_ignores - - -

-
df_drop_ignores(df, ignore)
-
- -
- -

Checks for rows containing ignore, dropping them in place if any.

- -

Parameters:

- - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
df - pd.DataFrame -

The original dataframe.

- required -
ignore - str -

The regex pattern that is in descriptions to ignore.

- required -
- -

Returns:

- - - - - - - - - - - - - -
TypeDescription
- pd.DataFrame -

A[n] pd.DataFrame without any rows containing ignore.

- -
- Source code in src/xlbudget/rwxlb.py -
def df_drop_ignores(df: pd.DataFrame, ignore: str) -> pd.DataFrame:
-    """Checks for rows containing `ignore`, dropping them in place if any.
-
-    Args:
-        df (pd.DataFrame): The original dataframe.
-        ignore (str): The regex pattern that is in descriptions to ignore.
-
-    Returns:
-        A[n] `pd.DataFrame` without any rows containing `ignore`.
-    """
-    ignored = df["Description"].str.contains(ignore)
-    ignores = df[ignored]
-    if not ignores.empty:
-        logger.warning(f"Dropping ignored transactions:\n{ignores}")
-        return df[~ignored].reset_index(drop=True)
-    return df
-
-
-
- -
- -
- - - -

- df_drop_na - - -

-
df_drop_na(df)
-
- -
- -

Checks for rows that contain only na values, dropping them in place if any.

- -

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
df - pd.DataFrame -

The original dataframe.

- required -
- -

Returns:

- - - - - - - - - - - - - -
TypeDescription
- pd.DataFrame -

A[n] pd.DataFrame without any rows that are entirely na.

- -
- Source code in src/xlbudget/rwxlb.py -
def df_drop_na(df: pd.DataFrame) -> pd.DataFrame:
-    """Checks for rows that contain only `na` values, dropping them in place if any.
-
-    Args:
-        df (pd.DataFrame): The original dataframe.
-
-    Returns:
-        A[n] `pd.DataFrame` without any rows that are entirely `na`.
-    """
-    na = df.isna().all(axis=1)
-    nas = df[na]
-    if not nas.empty:
-        logger.info(f"Dropping rows that contain only `na` values:\n{nas}")
-        return df[~na].reset_index(drop=True)
-    return df
-
-
-
- -
- -
- - - -

- update_xlbudget - - -

-
update_xlbudget(wb, df)
-
- -
- -

Updates an xlbudget file.

- -

Parameters:

- - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
wb - openpyxl.workbook.workbook.Workbook -

The xlbudget workbook.

- required -
df - pd.DataFrame -

The input file dataframe.

- required -
- -
- Source code in src/xlbudget/rwxlb.py -
def update_xlbudget(wb: Workbook, df: pd.DataFrame):
-    """Updates an xlbudget file.
-
-    Args:
-        wb (openpyxl.workbook.workbook.Workbook): The xlbudget workbook.
-        df (pd.DataFrame): The input file dataframe.
-    """
-    oldest_date, newest_date = df[df.columns[0]].agg(["min", "max"])
-    logger.debug(f"{oldest_date=}, {newest_date=}")
-
-    # create year sheets as needed
-    for year in range(oldest_date.year, newest_date.year + 1):
-        if str(year) not in wb.sheetnames:
-            logger.info(f"Creating {year} sheet")
-            create_year_sheet(wb, year)
-
-    # initialize table positions dictionary
-    # maps worksheet names to dictionaries that map table names to their position.
-    table_pos: Dict[str, Dict[str, TablePosition]] = {}
-    for year in range(oldest_date.year, newest_date.year + 1):
-        sheet_name = str(year)
-        table_pos[sheet_name] = {}
-
-        start_month = oldest_date.month if year == oldest_date.year else 1
-        end_month = newest_date.month if year == newest_date.year else 12
-        for month in range(start_month, end_month + 1):
-            month_name = calendar.month_name[month]
-            table_name = _get_month_table_name(month=month_name, year=sheet_name)
-            logger.debug(f"Initializing table {table_name} in sheet {sheet_name}")
-            ref = wb[sheet_name].tables[table_name].ref
-            table_pos[sheet_name][table_name] = TablePosition(ref)
-
-    # update df with transactions in wb
-    logger.debug(f"{df.shape=} before checking existing transactions")
-    for sheet_name in table_pos.keys():
-        ws = wb[sheet_name]
-
-        for pos in table_pos[sheet_name].values():
-            is_populated = bool(ws.cell(row=pos.next_row, column=pos.first_col).value)
-            if is_populated:
-                for r in range(pos.next_row, pos.initial_last_row + 1):
-                    transaction = []
-                    for i in range(len(MONTH_COLUMNS)):
-                        c = pos.first_col + i
-                        transaction.append(ws.cell(row=r, column=c).value)
-
-                    logger.debug(f"Appending {transaction=} to dataframe")
-                    # ignore mypy error and implicitly cast to df.dtypes
-                    df.loc[len(df) + 1] = transaction  # type: ignore[call-overload]
-    df = df_drop_duplicates(df)
-    # re-sort transactions to make the oldest transactions come first
-    df = df.sort_values(by=list(df.columns), ascending=True)
-    logger.debug(f"{df.shape=} after checking existing transactions")
-
-    # write dataframe to wb
-    for row in df.itertuples(index=False):
-        logger.debug(f"Writing transaction {row} to workbook")
-
-        # get worksheet and table position
-        sheet_name, month_name = str(row.Date.year), calendar.month_name[row.Date.month]
-        table_name = _get_month_table_name(month=month_name, year=sheet_name)
-        ws, pos = wb[sheet_name], table_pos[sheet_name][table_name]
-
-        # set date cell
-        date_cell = ws.cell(row=pos.next_row, column=pos.first_col)
-        date_cell.value = row.Date
-        date_cell.number_format = MONTH_COLUMNS[0].format
-
-        # set description cell
-        ws.cell(row=pos.next_row, column=pos.first_col + 1).value = row.Description
-
-        # set amount cell
-        amount_cell = ws.cell(row=pos.next_row, column=pos.first_col + 2)
-        amount_cell.value = row.Amount
-        amount_cell.number_format = MONTH_COLUMNS[2].format
-
-        pos.next_row += 1
-
-    # update table refs
-    for sheet_name in table_pos.keys():
-        for table_name, pos in table_pos[sheet_name].items():
-            tab = wb[sheet_name].tables[table_name]
-            ref = pos.get_ref()
-            if ref != tab.ref:
-                logger.debug(
-                    f"Updating ref of table {tab.name} from {tab.ref} to {ref}"
-                )
-                tab.ref = pos.get_ref()
-
-
-
- -
- - - -
- -
- -
- - - - - - -
-
- - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pr-24/search/search_index.json b/pr-24/search/search_index.json deleted file mode 100644 index bd9c665..0000000 --- a/pr-24/search/search_index.json +++ /dev/null @@ -1 +0,0 @@ -{"config":{"lang":["en"],"separator":"[\\s\\-,:!=\\[\\]()\"/]+|(?!\\b)(?=[A-Z][a-z])|\\.(?!\\d)|&[lg]t;","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Excel Budget","text":"

Xlbudget is a personal bookkeeping assistant that is in active development.

"},{"location":"coverage/","title":"Coverage Report","text":""},{"location":"license/","title":"License","text":"
Copyright (c) 2016 The Python Packaging Authority (PyPA)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\nof the Software, and to permit persons to whom the Software is furnished to do\nso, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n
"},{"location":"developer_guide/contributing/","title":"Contributing","text":"

This repository's infrastructure features pinned dependency management, a documentation site, an automated release process, GitHub integration, VS Code integration, and much more.

"},{"location":"developer_guide/contributing/#setup-for-local-development","title":"Setup for Local Development","text":"
  1. Install tox
  2. Clone the repository
"},{"location":"developer_guide/contributing/#tox","title":"Tox","text":"

tox is used to automate and standardize testing across local development environments and CI/CD pipelines.

"},{"location":"developer_guide/contributing/#tox-configuration","title":"Tox Configuration","text":"

The tox configuration for this repository can be found in tox.ini.

"},{"location":"developer_guide/contributing/#tox-environments","title":"Tox Environments","text":"

Each tox environment accomplishes a specific purpose. List all tox environments and their descriptions with tox list.

Details about each environment are given below:

  • py*: for a particular python version, it
    1. checks if the package can be built (may be commented out),
    2. runs the linters,
    3. runs the test suite, and
    4. generates a coverage report source file .coverage
  • check-release: checks that the package is ready to be released
  • coverage: converts .coverage to human readable formats
    • html: used to create the Coverage Report page
    • json: used to create the coverage badge in the README
  • dev: used to create a development environment with all dependencies installed
    • When in the development environment, the commands that are run in each environment can be run in your terminal
  • docs-build: builds the docs to ensure that they are in a valid state
  • docs-serve: runs the docs development server
  • format: runs the formatters
  • update: updates the dependencies without upgrading
  • upgrade: updates the dependencies
"},{"location":"developer_guide/contributing/#running-tox-environments","title":"Running Tox Environments","text":"
  • tox -e <environment> will run a single environment
  • tox will run all the default environments as noted by tox list
    • To set an environment as default, add it to envlist in tox.ini

Known issues running tox environments:

Environment Issue Solution coverage coverage combine outputs \"No data to combine\" coverage cannot be run independently, as it needs .coverage from testenv: run tox instead. If you are still getting this error, remove .coverage and rerun."},{"location":"developer_guide/contributing/#tox-development-environments","title":"Tox Development Environments","text":"

The tox devenv command will create a virtual environment and install the environment's dependencies in it.

  • To create a virtual environment with all dependencies installed, run tox devenv -e dev .venv.
  • Using a virtual environment: activate Python virtual environments
"},{"location":"developer_guide/contributing/#dependencies","title":"Dependencies","text":"

Dependencies are defined in pyproject.toml. They are pinned and managed using pip-tools. The pinned dependencies can be found in requirements/.

"},{"location":"developer_guide/contributing/#how-to-add-or-update-dependencies","title":"How to Add or Update Dependencies","text":"
  1. To add a dependency, add it in pyproject.toml; where you add the dependency depends on what type of dependency it is:
    • Add project dependencies to the dependencies list
    • Add environment-specific dependencies to the corresponding list below [project.optional-dependencies]
  2. Run the update tox environment: tox -e update
    • If you want to upgrade dependencies as well, run this instead: tox -e upgrade
  3. Verify that the tests still pass: tox
  4. If you are using the development environment, recreate it: tox devenv -e dev .venv
  5. Commit and push the changes
"},{"location":"developer_guide/docs/","title":"Docs","text":"

The Docs were created using Material for MkDocs, a Markdown static site generator with a material design theme.

"},{"location":"developer_guide/docs/#running-docs-locally","title":"Running Docs Locally","text":"
  1. Create and use the dev development environment
  2. Run the development server: mkdocs serve
    • If you are using VS Code, see the VS Code Integration page
"},{"location":"developer_guide/docs/#building-for-offline-usage","title":"Building for Offline Usage","text":"

To build for offline usage, uncomment the offline plugin in mkdocs.yml before running mkdocs build. For what this does, refer to the related Material for Mkdocs docs page.

"},{"location":"developer_guide/docs/#features","title":"Features","text":""},{"location":"developer_guide/docs/#automatic-documentation-from-sources","title":"Automatic Documentation from Sources","text":"

mkdocstrings was used to create the Code Reference section of the Docs.

incompatible theme.features

mkdocstrings is not compatible with theme.features.navigation.indexes.

"},{"location":"developer_guide/docs/#versioning","title":"Versioning","text":"

The Docs site has the following versions:

  • Version from branches
    • main: aliased to latest
      • Whenever you are on a version other than latest, a warning will be displayed above the header
    • pr-<pr number>:
      • Created when a pull request is opened
      • Updated when the pull request commits are modified
      • Deleted when the pull request is closed
  • Version from releases: <x.x>
"},{"location":"developer_guide/github_actions/","title":"GitHub Actions","text":""},{"location":"developer_guide/github_actions/#ci-workflow","title":"CI Workflow","text":"

The CI workflow contains the jobs that run in pull requests and pushes to the main branch:

  • tests: run tox on various Python versions and operating systems
  • coverage: generates code coverage reports for the docs site and README status badge
  • docs-build: builds the docs site
  • markdownlint: lints Markdown files
  • markdown-link-check: checks links in Markdown files work
  • docs-deploy and docs-delete: automate docs versioning

Notes:

  • tox is used to ensure that results can be replicated locally
  • Not all jobs run every workflow call: see the if keyword in each job
  • Some jobs depend on other jobs: see the needs keyword in each job
  • Artifacts are used to access the coverage reports generated by tests in coverage: storing workflow data as artifacts
CI workflow source code

.github/workflows/ci.yml

name: CI\non:\npull_request:\n# default types + closed\ntypes: [opened, synchronize, reopened, closed]\npush:\nbranches:\n- main\ndefaults:\nrun:\nshell: bash\nenv:\nPIP_DISABLE_PIP_VERSION_CHECK: 1\npermissions:\ncontents: write\njobs:\ntests:\nname: \"Run tests using python-${{ matrix.python-version }} on ${{ matrix.os }}\"\nruns-on: \"${{ matrix.os }}\"\nif: github.event_name != 'pull_request' || github.event.action != 'closed'\nstrategy:\nfail-fast: false\nmatrix:\nos: [ubuntu-latest, macos-latest, windows-latest]\npython-version: ['3.8', '3.9', '3.10', '3.11']\nsteps:\n- name: \"Check out the repo\"\nuses: \"actions/checkout@v4\"\n- name: \"Set up Python\"\nuses: \"actions/setup-python@v5\"\nwith:\npython-version: \"${{ matrix.python-version }}\"\ncache: pip\ncache-dependency-path: '**/requirements/test.txt'\n- name: \"Install dependencies\"\nrun: python -m pip install tox tox-gh-actions\n- name: \"Run tox for python-${{ matrix.python-version }} on ${{ matrix.os }}\"\nrun: python -m tox\n- name: \"Combine coverage data from tests\"\nif: (matrix.python-version == '3.10') && (matrix.os == 'ubuntu-latest')\nrun: |\npython -m tox -e coverage\nexport TOTAL=$(python -c \"import json;print(json.load(open('coverage.json'))['totals']['percent_covered_display'])\")\necho \"total=$TOTAL\" >> $GITHUB_ENV\necho \"### Total coverage: ${TOTAL}%\" >> $GITHUB_STEP_SUMMARY\n- name: \"Make coverage badge\"\nif: (matrix.python-version == '3.10') && (matrix.os == 'ubuntu-latest') && (github.repository == 'patrick-5546/xlbudget') && (github.ref == 'refs/heads/main')\n# https://gist.github.com/patrick-5546/845b19d91f3d03c94677f6fae6eb414c\nuses: schneegans/dynamic-badges-action@v1.7.0\nwith:\n# GIST_TOKEN is a GitHub personal access token with scope \"gist\".\nauth: ${{ secrets.GIST_TOKEN }}\ngistID: 845b19d91f3d03c94677f6fae6eb414c   # replace with your real Gist id.\nfilename: covbadge-xlbudget.json\nlabel: Coverage\nmessage: ${{ env.total }}%\nminColorRange: 50\nmaxColorRange: 90\nvalColorRange: ${{ env.total }}\n- name: \"Upload HTML coverage report\"\nif: (matrix.python-version == '3.10') && (matrix.os == 'ubuntu-latest')\nuses: actions/upload-artifact@v4\nwith:\nname: htmlcov\npath: htmlcov/\ndocs-build:\nname: Build Docs\nruns-on: ubuntu-latest\nif: github.event_name != 'pull_request' || github.event.action != 'closed'\nsteps:\n- name: \"Check out the repo\"\nuses: \"actions/checkout@v4\"\n- name: \"Set up Python\"\nuses: \"actions/setup-python@v5\"\nwith:\npython-version: \"3.10\"\ncache: pip\ncache-dependency-path: '**/requirements/docs.txt'\n- name: \"Install dependencies\"\nrun: python -m pip install tox tox-gh-actions\n- name: \"Build docs\"\nrun: python -m tox -e docs-build\n# https://github.com/nosborn/github-action-markdown-cli\nmarkdownlint:\nname: Lint Markdown\nruns-on: ubuntu-latest\nif: github.event_name != 'pull_request' || github.event.action != 'closed'\nneeds: docs-build\nsteps:\n- name: Check out code\nuses: actions/checkout@v4\n- name: Lint markdown pages\nuses: nosborn/github-action-markdown-cli@v3\nwith:\nfiles: .\nconfig_file: '.markdownlint.json'\ndot: true\n# https://github.com/gaurav-nelson/github-action-markdown-link-check\nmarkdown-link-check:\nname: Check links in Markdown files\nruns-on: ubuntu-latest\nif: github.event_name != 'pull_request' || github.event.action != 'closed'\nneeds: docs-build\nsteps:\n- name: Check out code\nuses: actions/checkout@v4\n- name: Check markdown pages for broken links\nuses: gaurav-nelson/github-action-markdown-link-check@v1\nwith:\nconfig-file: '.mlc_config.json'\nfolder-path: '.'\n# https://squidfunk.github.io/mkdocs-material/publishing-your-site/#with-github-actions\ndocs-deploy:\nname: Deploy Docs version\nruns-on: ubuntu-latest\nif: github.event_name != 'pull_request' || github.event.action != 'closed'\nneeds: [tests, markdownlint, markdown-link-check]\nsteps:\n- name: Check out code\nuses: actions/checkout@v4\nwith:\n# checkout all commits to get accurate page revision times\n# for the git-revision-date-localized plugin\nfetch-depth: '0'\n- name: Setup Python\nuses: actions/setup-python@v5\nwith:\npython-version: \"3.10\"\ncache: pip\ncache-dependency-path: '**/requirements/docs.txt'\n- name: Download HTML coverage report\nuses: actions/download-artifact@v4\nwith:\nname: htmlcov\npath: htmlcov\n- name: Install dependencies\nrun: python -m pip install -r requirements/docs.txt\n- name: \"Deploy pr-${{ github.event.number }} version of the Docs\"\nif: github.event_name == 'pull_request'\nrun: |\ngit config user.name ci-bot\ngit config user.email ci-bot@example.com\nmike deploy --push pr-${{ github.event.number }}\n- name: Deploy main version of the Docs\nif: github.event_name == 'push' && github.ref == 'refs/heads/main'\nrun: |\ngit config user.name ci-bot\ngit config user.email ci-bot@example.com\nmike deploy --push --update-aliases main latest\ndocs-delete:\nname: Delete Docs version\nruns-on: ubuntu-latest\nif: github.event_name == 'pull_request' && github.event.action == 'closed'\nsteps:\n- name: Check out code\nuses: actions/checkout@v4\nwith:\n# checkout all commits and branches to get gh-pages\nfetch-depth: '0'\n- name: Setup Python\nuses: actions/setup-python@v5\nwith:\npython-version: \"3.10\"\ncache: pip\ncache-dependency-path: '**/requirements/docs.txt'\n- name: Install dependencies\nrun: python -m pip install -r requirements/docs.txt\n- name: \"Delete pr-${{ github.event.number }} version of the Docs\"\nrun: |\ngit config user.name ci-bot\ngit config user.email ci-bot@example.com\nmike delete --push pr-${{ github.event.number }}\n
"},{"location":"developer_guide/github_actions/#release-workflow","title":"Release Workflow","text":"

TBD.

Release workflow source code

.github/workflows/release.yml

# this file is *not* meant to cover or endorse the use of GitHub Actions, but rather to\n# help make automated releases for this project\nname: Release\non:\nrelease:\ntypes: [published]\njobs:\nbuild-and-publish:\nruns-on: ubuntu-latest\nsteps:\n- name: Checkout\nuses: actions/checkout@v4\n- name: Set up Python\nuses: actions/setup-python@v5\nwith:\npython-version: '3.x'\n- name: Install build dependencies\nrun: python -m pip install -U setuptools wheel build\n- name: Build\nrun: python -m build .\n- name: Publish\nuses: pypa/gh-action-pypi-publish@master\nwith:\npassword: ${{ secrets.PYPI_API_TOKEN }}\n
"},{"location":"developer_guide/releases/","title":"Releases","text":""},{"location":"developer_guide/releases/#preparing-for-your-first-release","title":"Preparing for your first release","text":"
  1. Go through pyproject.toml and update the relevant fields
  2. In tox.ini, set check-release as a default environment by adding it to envlist
  3. In this file, uncomment badges and uncomment and update their URLs with the package name where applicable
  4. Create a repository secret named PYPI_API_TOKEN with the corresponding value
"},{"location":"developer_guide/releases/#publishing-a-new-release","title":"Publishing a new release","text":"

TBD.

"},{"location":"developer_guide/vscode/","title":"VS Code","text":""},{"location":"developer_guide/vscode/#setup-for-vs-code","title":"Setup for VS Code","text":"
  1. cd to the root directory of the repository
  2. Create the dev development environment: tox devenv -e dev .venv
  3. Open the repository in VS Code: code .
  4. Install the recommended extensions
"},{"location":"developer_guide/vscode/#configuration-files","title":"Configuration Files","text":"

VS Code configuration files can be found in the .vscode/ directory.

  • cspell.json: configuration for the spell checker
  • extensions.json: recommended extensions
    • See the recommended extensions by searching for \"@recommended\" in the Extensions view
  • google_docstring_custom_template.mustache: a custom docstrings template for autoDocstring until this issue is resolved
  • launch.json: launch configurations
    • Run the launch configurations from the Run and Debug view
  • settings.json: settings
"},{"location":"developer_guide/vscode/#shortcuts","title":"Shortcuts","text":"

Useful VS Code shortcuts that aren't specific to this repository.

MacOS Shortcuts

For keyboard shortcuts on MacOS, substitute Ctrl with Cmd.

  • Open Quick Open: Ctrl+P
    • Search for files
    • Open the command palette by typing \"> \"
    • Search for tasks by typing \"task \"
    • Search for launch configurations by typing \"debug \"
  • Open Command Palette: Ctrl+Shift+P
"},{"location":"developer_guide/vscode/#integrations","title":"Integrations","text":""},{"location":"developer_guide/vscode/#docs","title":"Docs","text":"

Related recommended extensions enhance Markdown file previews, check for markdownlint errors, enhance VS Code Markdown support, add autocomplete for mkdocs.yml, and more:

  • The enhanced Markdown file preview replaces VS Code's built-in preview
  • The configuration file for markdownlint is .markdownlint.json
  • Format tables in a Markdown file with Alt+Shift+F
    • On Linux, the shortcut is Ctrl+Shift+I

There are also launch configurations to run the development server and open browsers:

Launch Configuration Description Serve Docs runs mkdocs serve Open Docs in Chrome runs mkdocs serve and open Docs in Chrome Open Docs in Edge runs mkdocs serve and open Docs in Microsoft Edge"},{"location":"developer_guide/vscode/#python","title":"Python","text":"

Related recommended extensions improve autocomplete, format on save, lint, test, and more:

  • Adds autocomplete for docstrings, type hints, and functions
  • Runs the black and isort formatters when a file is saved
  • Runs the flake8 and mypy linters when a file is saved and after formatters run
  • Run tests from the Testing view
"},{"location":"developer_guide/vscode/#common-extensions-settings","title":"Common Extensions & Settings","text":"

Other recommended extensions further improve autocomplete (AI, file paths), check spelling, and show the commit and author who last modified the current line, and more:

  • Mark a word as spelled correct by hovering over it, selecting Quick Fix..., then selecting Add \"<word>\" to config: .vscode/cspell.json
  • Hover over the current line blame annoation at the end of the line for more details

Common settings:

  • Make whitespace at the end of the line visible
  • Ruler at the line length limit
"},{"location":"reference/SUMMARY/","title":"SUMMARY","text":"
  • xlbudget
    • commands
    • configure
    • inputformat
    • rwxlb
"},{"location":"reference/xlbudget/","title":"xlbudget","text":"

Xlbudget: a personal bookkeeping assistant.

"},{"location":"reference/xlbudget/#xlbudget-functions","title":"Functions","text":""},{"location":"reference/xlbudget/#xlbudget.main","title":"main","text":"
main()\n

Entry point for the application script.

Source code in src/xlbudget/__init__.py
def main():\n\"Entry point for the application script.\"\nargs = setup()\ncmd = args.init(args)\ncmd.run()\n
"},{"location":"reference/xlbudget/commands/","title":"commands","text":"

The commands, implemented as implementations of the abstract class Command.

"},{"location":"reference/xlbudget/commands/#xlbudget.commands-classes","title":"Classes","text":""},{"location":"reference/xlbudget/commands/#xlbudget.commands.Command","title":"Command","text":"

Bases: ABC

The abstract class that the command implementations implement.

Class Attributes

Name Type Description default_path str

The default path of the xlbudget file.

Attributes:

Name Type Description trial bool

If True, the xlbudget file will not be written to.

path str

The path to the xlbudget file.

Source code in src/xlbudget/commands.py
class Command(ABC):\n\"\"\"The abstract class that the command implementations implement.\n    Attributes: Class Attributes\n        default_path (str): The default path of the xlbudget file.\n    Attributes:\n        trial (bool): If True, the xlbudget file will not be written to.\n        path (str): The path to the xlbudget file.\n    \"\"\"\ndefault_path: str = \"xlbudget.xlsx\"\n@property\n@abstractmethod\ndef name(self) -> str:\n\"\"\"Ensures that the `name` class attribute is defined in subclasses.\n        Part 1/2 of the abstract attribute implementation of `name`.\n        Reference: https://stackoverflow.com/a/53417582.\n        \"\"\"\nraise NotImplementedError\ndef get_name(self) -> str:\n\"\"\"Used to access the `name` class attribute defined in subclasses.\n        Part 2/2 of the abstract attribute implementation of `name`.\n        Reference: https://stackoverflow.com/a/53417582.\n        \"\"\"\nreturn self.name\n@property\n@abstractmethod\ndef aliases(self) -> List[str]:\n\"\"\"Ensures that the `aliases` class attribute is defined in subclasses.\n        Part 1/2 of the abstract attribute implementation of `aliases`.\n        Reference: https://stackoverflow.com/a/53417582.\n        \"\"\"\nraise NotImplementedError\ndef get_aliases(self) -> List[str]:\n\"\"\"Used to access the `aliases` class attribute defined in subclasses.\n        Part 2/2 of the abstract attribute implementation of `aliases`.\n        Reference: https://stackoverflow.com/a/53417582.\n        \"\"\"\nreturn self.aliases\n@classmethod\ndef configure_common_args(cls, parser: ArgumentParser) -> None:\n\"\"\"Configures the arguments that are used by all commands.\n        Args:\n            parser (ArgumentParser): The argument parser.\n        \"\"\"\nparser.add_argument(\n\"-t\",\n\"--trial\",\naction=\"store_true\",\nhelp=\"try a command without generating/updating the xlbudget file\",\n)\nparser.add_argument(\n\"-p\",\n\"--path\",\nhelp=\"path to the xlbudget file (default: %(default)s)\",\ndefault=cls.default_path,\n)\n@classmethod\n@abstractmethod\ndef configure_args(cls, subparsers: _SubParsersAction) -> None:\npass\n@abstractmethod\ndef __init__(self, args: Namespace) -> None:\nself.trial = args.trial\nself._check_path(args.path)\nself.path = args.path\n@staticmethod\ndef _check_path(path: str) -> None:\n\"\"\"Check that `path` is a valid path to an xlbudget file.\n        Args:\n            path (str): The xlbudget path.\n        Raises:\n            ValueError: If `path` is not a XLSX file.\n            FileNotFoundError: If `path` is not in an existing directory.\n        \"\"\"\nxlsx_ext = \".xlsx\"\nif not path.endswith(xlsx_ext):\nraise ValueError(f\"Path '{path}' does not end with '{xlsx_ext}'\")\ndir = os.path.dirname(path)\nif dir and not os.path.isdir(dir):\nraise FileNotFoundError(f\"Directory '{dir}' does not exist\")\n@abstractmethod\ndef run(self) -> None:\npass\n
"},{"location":"reference/xlbudget/commands/#xlbudget.commands.Command-attributes","title":"Attributes","text":""},{"location":"reference/xlbudget/commands/#xlbudget.commands.Command.aliases","title":"aliases property abstractmethod","text":"
aliases: List[str]\n

Ensures that the aliases class attribute is defined in subclasses. Part \u00bd of the abstract attribute implementation of aliases. Reference: https://stackoverflow.com/a/53417582.

"},{"location":"reference/xlbudget/commands/#xlbudget.commands.Command.name","title":"name property abstractmethod","text":"
name: str\n

Ensures that the name class attribute is defined in subclasses. Part \u00bd of the abstract attribute implementation of name. Reference: https://stackoverflow.com/a/53417582.

"},{"location":"reference/xlbudget/commands/#xlbudget.commands.Command-functions","title":"Functions","text":""},{"location":"reference/xlbudget/commands/#xlbudget.commands.Command._check_path","title":"_check_path staticmethod","text":"
_check_path(path)\n

Check that path is a valid path to an xlbudget file.

Parameters:

Name Type Description Default path str

The xlbudget path.

required

Raises:

Type Description ValueError

If path is not a XLSX file.

FileNotFoundError

If path is not in an existing directory.

Source code in src/xlbudget/commands.py
@staticmethod\ndef _check_path(path: str) -> None:\n\"\"\"Check that `path` is a valid path to an xlbudget file.\n    Args:\n        path (str): The xlbudget path.\n    Raises:\n        ValueError: If `path` is not a XLSX file.\n        FileNotFoundError: If `path` is not in an existing directory.\n    \"\"\"\nxlsx_ext = \".xlsx\"\nif not path.endswith(xlsx_ext):\nraise ValueError(f\"Path '{path}' does not end with '{xlsx_ext}'\")\ndir = os.path.dirname(path)\nif dir and not os.path.isdir(dir):\nraise FileNotFoundError(f\"Directory '{dir}' does not exist\")\n
"},{"location":"reference/xlbudget/commands/#xlbudget.commands.Command.configure_common_args","title":"configure_common_args classmethod","text":"
configure_common_args(parser)\n

Configures the arguments that are used by all commands.

Parameters:

Name Type Description Default parser ArgumentParser

The argument parser.

required Source code in src/xlbudget/commands.py
@classmethod\ndef configure_common_args(cls, parser: ArgumentParser) -> None:\n\"\"\"Configures the arguments that are used by all commands.\n    Args:\n        parser (ArgumentParser): The argument parser.\n    \"\"\"\nparser.add_argument(\n\"-t\",\n\"--trial\",\naction=\"store_true\",\nhelp=\"try a command without generating/updating the xlbudget file\",\n)\nparser.add_argument(\n\"-p\",\n\"--path\",\nhelp=\"path to the xlbudget file (default: %(default)s)\",\ndefault=cls.default_path,\n)\n
"},{"location":"reference/xlbudget/commands/#xlbudget.commands.Command.get_aliases","title":"get_aliases","text":"
get_aliases()\n

Used to access the aliases class attribute defined in subclasses. Part 2/2 of the abstract attribute implementation of aliases. Reference: https://stackoverflow.com/a/53417582.

Source code in src/xlbudget/commands.py
def get_aliases(self) -> List[str]:\n\"\"\"Used to access the `aliases` class attribute defined in subclasses.\n    Part 2/2 of the abstract attribute implementation of `aliases`.\n    Reference: https://stackoverflow.com/a/53417582.\n    \"\"\"\nreturn self.aliases\n
"},{"location":"reference/xlbudget/commands/#xlbudget.commands.Command.get_name","title":"get_name","text":"
get_name()\n

Used to access the name class attribute defined in subclasses. Part 2/2 of the abstract attribute implementation of name. Reference: https://stackoverflow.com/a/53417582.

Source code in src/xlbudget/commands.py
def get_name(self) -> str:\n\"\"\"Used to access the `name` class attribute defined in subclasses.\n    Part 2/2 of the abstract attribute implementation of `name`.\n    Reference: https://stackoverflow.com/a/53417582.\n    \"\"\"\nreturn self.name\n
"},{"location":"reference/xlbudget/commands/#xlbudget.commands.Update","title":"Update","text":"

Bases: Command

The update command updates an existing xlbudget file.

Class Attributes

Name Type Description name str

The command's CLI name.

aliases List[str]

The command's CLI aliases.

Attributes:

Name Type Description input Optional[str]

The path to the input file, otherwise paste in terminal.

format inputformat.InputFormat

The input file format.

year Optional[str]

The year all transactions were made, only relevant if the input format is 'BMO_CC_ADOBE'.

Source code in src/xlbudget/commands.py
class Update(Command):\n\"\"\"The `update` command updates an existing xlbudget file.\n    Attributes: Class Attributes\n        name (str): The command's CLI name.\n        aliases (List[str]): The command's CLI aliases.\n    Attributes:\n        input (Optional[str]): The path to the input file, otherwise paste in terminal.\n        format (inputformat.InputFormat): The input file format.\n        year (Optional[str]): The year all transactions were made, only relevant if\n            the input format is 'BMO_CC_ADOBE'.\n    \"\"\"\nname: str = \"update\"\naliases: List[str] = [\"u\"]\n@classmethod\ndef configure_args(cls, subparsers: _SubParsersAction) -> None:\n\"\"\"Configures the argument parser for the `update` command.\n        Args:\n            subparsers (_SubParsersAction): The command `subparsers`.\n        \"\"\"\nparser = _add_parser(\nsubparsers,\nname=cls.name,\naliases=cls.aliases,\nhelp=\"update an existing xlbudget file\",\ncmd_cls=Update,\n)\n# required arguments\nparser.add_argument(\n\"format\",\naction=GetInputFormats,\nchoices=GetInputFormats.input_formats.keys(),\nhelp=\"select an input format\",\n)\n# optional arguments\nparser.add_argument(\"-i\", \"--input\", help=\"path to the input file\")\nparser.add_argument(\n\"-y\",\n\"--year\",\nhelp=\"year that all transactions were made, only relevant if input format \"\n\"is 'BMO_CC_ADOBE'\",\n)\ndef __init__(self, args: Namespace) -> None:\nsuper().__init__(args)\nself._check_input(args.input, args.format, args.year)\nself.input = args.input\nself.format = args.format\nself.year = args.year\nlogger.debug(f\"instance variables: {vars(self)}\")\n@staticmethod\ndef _check_input(\ninput: Optional[str], input_format: Optional[InputFormat], year: Optional[str]\n) -> None:\n\"\"\"Check that `input` and `year` are valid.\n        Args:\n            input (Optional[str]): The input path.\n            input_format (Optional[InputFormat]): The input format.\n            year (Optional[str]): The year of all transactions.\n        Raises:\n            ValueError: If `input` is not None and the wrong file extension or DNE.\n            ValueError: If `year` is None when `input_format` is 'BMO_CC_ADOBE'.\n        \"\"\"\nif input is None:\nreturn\nin_ext = (\".csv\", \".tsv\", \".txt\")\nif not input.endswith(in_ext):\nraise ValueError(f\"Input '{input}' does not end with one of '{in_ext}'\")\nif not os.path.isfile(input):\nraise ValueError(f\"Input '{input}' is not an existing file\")\n# get key from value: https://stackoverflow.com/a/13149770\nif input_format is not None:\n# validate year\nformat = list(GetInputFormats.input_formats.keys())[\nlist(GetInputFormats.input_formats.values()).index(input_format)\n]\nif format == \"BMO_CC_ADOBE\" and year is None:\nraise ValueError(f\"Must specify 'year' argument when {format=}\")\n# validate input file type in more detail\nif input.endswith(\".csv\") and not input_format.seperator == \",\":\nraise ValueError(f\"Input file should be CSV for {format=}\")\nif input.endswith(\".tsv\") and not input_format.seperator == \"\\t\":\nraise ValueError(f\"Input file should be TSV for {format=}\")\ndef run(self) -> None:\nlogger.info(f\"Parsing input {self.input}\")\ndf = parse_input(self.input, self.format, self.year)\nlogger.debug(f\"input file: {df.shape=}, df.dtypes=\\n{df.dtypes}\")\nlogger.debug(f\"df.head()=\\n{df.head()}\")\nif os.path.exists(self.path):\nlogger.info(f\"Loading xlbudget file {self.path}\")\nwb = load_workbook(self.path)\nelse:\nlogger.warning(f\"xlbudget file {self.path} does not exist, creating\")\nwb = Workbook()\nws = wb.active\n# ignore type mismatch of active worksheet\nwb.remove(ws)  # type: ignore[arg-type]\nlogger.info(\"Updating xlbudget file\")\nupdate_xlbudget(wb, df)\nif not self.trial:\nlogger.info(f\"Saving xlbudget file to {self.path}\")\nwb.save(self.path)\nelse:\nlogger.info(f\"Trial run: not saving xlbudget file to {self.path}\")\n
"},{"location":"reference/xlbudget/commands/#xlbudget.commands.Update-functions","title":"Functions","text":""},{"location":"reference/xlbudget/commands/#xlbudget.commands.Update._check_input","title":"_check_input staticmethod","text":"
_check_input(input, input_format, year)\n

Check that input and year are valid.

Parameters:

Name Type Description Default input Optional[str]

The input path.

required input_format Optional[InputFormat]

The input format.

required year Optional[str]

The year of all transactions.

required

Raises:

Type Description ValueError

If input is not None and the wrong file extension or DNE.

ValueError

If year is None when input_format is 'BMO_CC_ADOBE'.

Source code in src/xlbudget/commands.py
@staticmethod\ndef _check_input(\ninput: Optional[str], input_format: Optional[InputFormat], year: Optional[str]\n) -> None:\n\"\"\"Check that `input` and `year` are valid.\n    Args:\n        input (Optional[str]): The input path.\n        input_format (Optional[InputFormat]): The input format.\n        year (Optional[str]): The year of all transactions.\n    Raises:\n        ValueError: If `input` is not None and the wrong file extension or DNE.\n        ValueError: If `year` is None when `input_format` is 'BMO_CC_ADOBE'.\n    \"\"\"\nif input is None:\nreturn\nin_ext = (\".csv\", \".tsv\", \".txt\")\nif not input.endswith(in_ext):\nraise ValueError(f\"Input '{input}' does not end with one of '{in_ext}'\")\nif not os.path.isfile(input):\nraise ValueError(f\"Input '{input}' is not an existing file\")\n# get key from value: https://stackoverflow.com/a/13149770\nif input_format is not None:\n# validate year\nformat = list(GetInputFormats.input_formats.keys())[\nlist(GetInputFormats.input_formats.values()).index(input_format)\n]\nif format == \"BMO_CC_ADOBE\" and year is None:\nraise ValueError(f\"Must specify 'year' argument when {format=}\")\n# validate input file type in more detail\nif input.endswith(\".csv\") and not input_format.seperator == \",\":\nraise ValueError(f\"Input file should be CSV for {format=}\")\nif input.endswith(\".tsv\") and not input_format.seperator == \"\\t\":\nraise ValueError(f\"Input file should be TSV for {format=}\")\n
"},{"location":"reference/xlbudget/commands/#xlbudget.commands.Update.configure_args","title":"configure_args classmethod","text":"
configure_args(subparsers)\n

Configures the argument parser for the update command.

Parameters:

Name Type Description Default subparsers _SubParsersAction

The command subparsers.

required Source code in src/xlbudget/commands.py
@classmethod\ndef configure_args(cls, subparsers: _SubParsersAction) -> None:\n\"\"\"Configures the argument parser for the `update` command.\n    Args:\n        subparsers (_SubParsersAction): The command `subparsers`.\n    \"\"\"\nparser = _add_parser(\nsubparsers,\nname=cls.name,\naliases=cls.aliases,\nhelp=\"update an existing xlbudget file\",\ncmd_cls=Update,\n)\n# required arguments\nparser.add_argument(\n\"format\",\naction=GetInputFormats,\nchoices=GetInputFormats.input_formats.keys(),\nhelp=\"select an input format\",\n)\n# optional arguments\nparser.add_argument(\"-i\", \"--input\", help=\"path to the input file\")\nparser.add_argument(\n\"-y\",\n\"--year\",\nhelp=\"year that all transactions were made, only relevant if input format \"\n\"is 'BMO_CC_ADOBE'\",\n)\n
"},{"location":"reference/xlbudget/commands/#xlbudget.commands-functions","title":"Functions","text":""},{"location":"reference/xlbudget/commands/#xlbudget.commands._add_parser","title":"_add_parser","text":"
_add_parser(subparsers, name, aliases, help, cmd_cls)\n

Adds an argument parser for a command. Any configuration that is common across commands should go here.

Parameters:

Name Type Description Default subparsers _SubParsersAction

The subparsers object.

required name str

The command name.

required aliases List[str]

The command aliases.

required help str

The command help message.

required cmd_cls Type[Command]

The command class.

required

Returns:

Type Description ArgumentParser

A[n] ArgumentParser for a command.

Source code in src/xlbudget/commands.py
def _add_parser(\nsubparsers: _SubParsersAction,\nname: str,\naliases: List[str],\nhelp: str,\ncmd_cls: Type[Command],\n) -> ArgumentParser:\n\"\"\"Adds an argument parser for a command. Any configuration that is common\n    across commands should go here.\n    Args:\n        subparsers (_SubParsersAction): The subparsers object.\n        name (str): The command name.\n        aliases (List[str]): The command aliases.\n        help (str): The command help message.\n        cmd_cls (Type[Command]): The command class.\n    Returns:\n        A[n] `ArgumentParser` for a command.\n    \"\"\"\nparser = subparsers.add_parser(name, aliases=aliases, help=help)\n# initialize the command with args.init(...)\nparser.set_defaults(init=cmd_cls)\nreturn parser\n
"},{"location":"reference/xlbudget/commands/#xlbudget.commands.get_command_classes","title":"get_command_classes","text":"
get_command_classes()\n

Gets all classes that implement the Command abstract class.

Returns:

Type Description List[Type[Command]]

A[n] List[Type[Command]] of all command classes.

Source code in src/xlbudget/commands.py
def get_command_classes() -> List[Type[Command]]:\n\"\"\"Gets all classes that implement the `Command` abstract class.\n    Returns:\n        A[n] `List[Type[Command]]` of all command classes.\n    \"\"\"\ncommand_module = sys.modules[__name__]\nreturn [getattr(command_module, c.__name__) for c in Command.__subclasses__()]\n
"},{"location":"reference/xlbudget/configure/","title":"configure","text":"

The setup and configuration for xlbudget.

Logger usage in this file

The logger can only be used after _configure_logger is called in setup.

"},{"location":"reference/xlbudget/configure/#xlbudget.configure-classes","title":"Classes","text":""},{"location":"reference/xlbudget/configure/#xlbudget.configure-functions","title":"Functions","text":""},{"location":"reference/xlbudget/configure/#xlbudget.configure._configure_argument_parser","title":"_configure_argument_parser","text":"
_configure_argument_parser()\n

Configures the argument parser for all arguments.

Returns:

Type Description ArgumentParser

A[n] ArgumentParser configured for this package.

Source code in src/xlbudget/configure.py
def _configure_argument_parser() -> ArgumentParser:\n\"\"\"Configures the argument parser for all arguments.\n    Returns:\n        A[n] `ArgumentParser` configured for this package.\n    \"\"\"\nparser = ArgumentParser()\nCommand.configure_common_args(parser)\n_configure_logger_args(parser)\ncmd_subparsers = parser.add_subparsers(\ntitle=\"command\",\nrequired=True,\ndescription=\"The xlbudget command to run.\",\n)\nfor cmd_cls in get_command_classes():\ncmd_cls.configure_args(cmd_subparsers)\nreturn parser\n
"},{"location":"reference/xlbudget/configure/#xlbudget.configure._configure_logger","title":"_configure_logger","text":"
_configure_logger(level)\n

Configures the logger.

Since this configuration is global, there is no need to return the logger. To use the logger in a file, add logger = logging.getLogger(__name__) at the top.

Parameters:

Name Type Description Default level int

The logging level.

required Source code in src/xlbudget/configure.py
def _configure_logger(level: int) -> None:\n\"\"\"Configures the logger.\n    Since this configuration is global, there is no need to return the logger.\n    To use the logger in a file, add `logger = logging.getLogger(__name__)` at the top.\n    Args:\n        level (int): The [logging level](https://docs.python.org/3/library/logging.html#logging-levels).\n    \"\"\"  # noqa\nlogging.basicConfig(\nlevel=level,\nformat=\"%(levelname)s - %(name)s:%(lineno)s - %(message)s\",\n)\n
"},{"location":"reference/xlbudget/configure/#xlbudget.configure._configure_logger_args","title":"_configure_logger_args","text":"
_configure_logger_args(parser)\n

Configures the argument parser for logger arguments. The log level configuration was adapted from this Stack Overflow answer.

Parameters:

Name Type Description Default parser ArgumentParser

The argument parser to update.

required Source code in src/xlbudget/configure.py
def _configure_logger_args(parser: ArgumentParser) -> None:\n\"\"\"Configures the argument parser for logger arguments.\n    The log level configuration was adapted from\n    [this Stack Overflow answer](https://stackoverflow.com/a/20663028).\n    Args:\n        parser (ArgumentParser): The argument parser to update.\n    \"\"\"\ngroup_log = parser.add_argument_group(\n\"logger configuration\",\ndescription=\"Arguments that override the default logger configuration.\",\n)\ngroup_log_lvl = group_log.add_mutually_exclusive_group()\ngroup_log_lvl.add_argument(\n\"-d\",\n\"--debug\",\nhelp=\"print lots of debugging statements; can't use with -v/--verbose\",\naction=\"store_const\",\ndest=\"log_level\",\nconst=logging.DEBUG,\ndefault=logging.WARNING,\n)\ngroup_log_lvl.add_argument(\n\"-v\",\n\"--verbose\",\nhelp=\"be verbose; can't use with -d/--debug\",\naction=\"store_const\",\ndest=\"log_level\",\nconst=logging.INFO,\n)\n
"},{"location":"reference/xlbudget/configure/#xlbudget.configure.setup","title":"setup","text":"
setup()\n

Package-level setup and configuration.

Returns:

Type Description Namespace

A[n] Namespace containing the parsed CLI arguments.

Source code in src/xlbudget/configure.py
def setup() -> Namespace:\n\"\"\"Package-level setup and configuration.\n    Returns:\n        A[n] `Namespace` containing the parsed CLI arguments.\n    \"\"\"\nparser = _configure_argument_parser()\nargs = parser.parse_args()\n_configure_logger(args.log_level)\n# log args after call to _configure_logger\nlogger = logging.getLogger(__name__)\nlogger.debug(f\"parsed CLI arguments: {args}\")\nreturn args\n
"},{"location":"reference/xlbudget/inputformat/","title":"inputformat","text":"

Input file format definitions.

"},{"location":"reference/xlbudget/inputformat/#xlbudget.inputformat-classes","title":"Classes","text":""},{"location":"reference/xlbudget/inputformat/#xlbudget.inputformat.GetInputFormats","title":"GetInputFormats","text":"

Bases: Action

Argparse action for the format argument. Adapted from this Stack Overflow answer.

Attributes:

Name Type Description input_formats Dict[str, InputFormat]

Maps format names to values.

Source code in src/xlbudget/inputformat.py
class GetInputFormats(Action):\n\"\"\"Argparse action for the format argument.\n    Adapted from [this Stack Overflow answer](https://stackoverflow.com/a/50799463).\n    Attributes:\n        input_formats (Dict[str, InputFormat]): Maps format names to values.\n    \"\"\"\ninput_formats: Dict[str, InputFormat] = {\nn: globals()[n] for n in globals() if isinstance(globals()[n], InputFormat)\n}\ndef __call__(self, parser, namespace, values, option_string=None):\nsetattr(namespace, self.dest, self.input_formats[values])\n
"},{"location":"reference/xlbudget/inputformat/#xlbudget.inputformat.InputFormat","title":"InputFormat","text":"

Bases: NamedTuple

Specifies the format of the input file.

Attributes:

Name Type Description header int

The 0-indexed row of the header in the input file.

names List[str]

The column names.

usecols List[int]

The first len(MONTH_COLUMNS) elements are indices of columns that map to MONTH_COLUMNS, there may indices after for columns required for post-processing.

ignores List[str]

Ignore transactions that contain with these regex patterns.

pre_processing Callable

The function to call before pd.read_csv().

post_processing Callable

The function to call after pd.read_csv().

sep str

The separator.

Source code in src/xlbudget/inputformat.py
class InputFormat(NamedTuple):\n\"\"\"Specifies the format of the input file.\n    Attributes:\n        header (int): The 0-indexed row of the header in the input file.\n        names (List[str]): The column names.\n        usecols (List[int]): The first len(`MONTH_COLUMNS`) elements are indices of\n            columns that map to `MONTH_COLUMNS`, there may indices after for columns\n            required for post-processing.\n        ignores (List[str]): Ignore transactions that contain with these regex patterns.\n        pre_processing (Callable): The function to call before `pd.read_csv()`.\n        post_processing (Callable): The function to call after `pd.read_csv()`.\n        sep (str): The separator.\n    \"\"\"\nheader: int\nnames: List[str]\nusecols: List[int]\nignores: List[str]\npre_processing: Callable = lambda input, year: input\npost_processing: Callable = lambda df: df\nseperator: str = \",\"\ndef get_usecols_names(self):\nreturn [self.names[i] for i in self.usecols[:3]]\n
"},{"location":"reference/xlbudget/inputformat/#xlbudget.inputformat-functions","title":"Functions","text":""},{"location":"reference/xlbudget/inputformat/#xlbudget.inputformat.bmo_acct_web_post_processing","title":"bmo_acct_web_post_processing","text":"
bmo_acct_web_post_processing(df)\n

Creates the \"Amount\" column.

Parameters:

Name Type Description Default df pd.DataFrame

The dataframe to process.

required

Returns:

Type Description pd.DataFrame

A[n] pd.DataFrame that combines \"Amount\" and \"Money in\" to create \"Amount\".

Source code in src/xlbudget/inputformat.py
def bmo_acct_web_post_processing(df: pd.DataFrame) -> pd.DataFrame:\n\"\"\"Creates the \"Amount\" column.\n    Args:\n        df (pd.DataFrame): The dataframe to process.\n    Returns:\n        A[n] `pd.DataFrame` that combines \"Amount\" and \"Money in\" to create \"Amount\".\n    \"\"\"\ndf[\"Amount\"] = df[\"Amount\"].replace(\"[$,]\", \"\", regex=True).astype(float)\ndf[\"Money in\"] = df[\"Money in\"].replace(\"[$,]\", \"\", regex=True).astype(float)\ndf[\"Amount\"] = np.where(df[\"Money in\"].isna(), df[\"Amount\"], df[\"Money in\"])\ndf = df.drop(\"Money in\", axis=1)\nreturn df\n
"},{"location":"reference/xlbudget/inputformat/#xlbudget.inputformat.bmo_cc_adobe_pre_processing","title":"bmo_cc_adobe_pre_processing","text":"
bmo_cc_adobe_pre_processing(_input, year)\n

Create CSV from input with each element on a new line.

Parameters:

Name Type Description Default _input Optional[str]

The file to process, if None then read from stdin.

required year str

The year of all transactions.

required

Returns:

Type Description io.StringIO

A[n] io.StringIO CSV.

Source code in src/xlbudget/inputformat.py
def bmo_cc_adobe_pre_processing(_input: Optional[str], year: str) -> io.StringIO:\n\"\"\"Create CSV from input with each element on a new line.\n    Args:\n        _input (Optional[str]): The file to process, if `None` then read from stdin.\n        year (str): The year of all transactions.\n    Returns:\n        A[n] `io.StringIO` CSV.\n    \"\"\"\n# get lines from stdin or file\nif _input is None:\nlines = []\nfor line in sys.stdin:\nlines.append(line.strip())\nelse:\nwith open(_input) as f:\nlines = f.read().splitlines()\nrows = []\ni = 0\nis_header = True\nwhile i < len(lines):\nelems = lines[i : i + 4]\n# reformat dates and add year\nif not is_header:\nelems[0] = year + datetime.strptime(elems[0], \"%b. %d\").strftime(\"-%m-%d\")\nelems[1] = year + datetime.strptime(elems[1], \"%b. %d\").strftime(\"-%m-%d\")\n# add negative sign to amounts that are not credited (CR on next line)\nif i + 4 < len(lines) and lines[i + 4] == \"CR\":\ni += 5\nelse:\nif not is_header:\nelems[-1] = \"-\" + elems[-1]\ni += 4\nrow = \"\\t\".join(elems) + \"\\n\"\nrows.append(row)\nif is_header:\nis_header = False\nnew_input = \"\".join(rows)\nreturn io.StringIO(new_input)\n
"},{"location":"reference/xlbudget/inputformat/#xlbudget.inputformat.bmo_cc_web_post_processing","title":"bmo_cc_web_post_processing","text":"
bmo_cc_web_post_processing(df)\n

Formats the \"Money in/out\" column.

Parameters:

Name Type Description Default df pd.DataFrame

The dataframe to process.

required

Returns:

Type Description pd.DataFrame

A[n] pd.DataFrame that converts \"Money in/out\" to a float.

Source code in src/xlbudget/inputformat.py
def bmo_cc_web_post_processing(df: pd.DataFrame) -> pd.DataFrame:\n\"\"\"Formats the \"Money in/out\" column.\n    Args:\n        df (pd.DataFrame): The dataframe to process.\n    Returns:\n        A[n] `pd.DataFrame` that converts \"Money in/out\" to a float.\n    \"\"\"\ndf[\"Money in/out\"] = (\ndf[\"Money in/out\"].replace(\"[$,]\", \"\", regex=True).astype(float)\n)\nreturn df\n
"},{"location":"reference/xlbudget/inputformat/#xlbudget.inputformat.parse_input","title":"parse_input","text":"
parse_input(input, format, year)\n

Parses an input.

Parameters:

Name Type Description Default input Optional[str]

The path to the input file, if None parse from stdin.

required format InputFormat

The input format.

required year Optional[str]

The year of all transactions.

required

Raises:

Type Description ValueError

If input contains duplicate transactions.

Returns:

Type Description pd.DataFrame

A[n] pd.DataFrame where the columns match the xlbudget file's column names.

Source code in src/xlbudget/inputformat.py
def parse_input(\ninput: Optional[str], format: InputFormat, year: Optional[str]\n) -> pd.DataFrame:\n\"\"\"Parses an input.\n    Args:\n        input (Optional[str]): The path to the input file, if None parse from stdin.\n        format (InputFormat): The input format.\n        year (Optional[str]): The year of all transactions.\n    Raises:\n        ValueError: If input contains duplicate transactions.\n    Returns:\n        A[n] `pd.DataFrame` where the columns match the xlbudget file's column names.\n    \"\"\"\ninput_initially_none = input is None\nif input_initially_none:\nprint(\"Paste your transactions here (CTRL+D twice on a blank line to end):\")\ninput = format.pre_processing(input, year)\ndf = pd.read_csv(\ninput if input is not None else sys.stdin,\nsep=format.seperator,\nindex_col=False,\nnames=format.names,\nheader=format.header if input is not None else None,\nusecols=format.usecols,\nparse_dates=[0],\nskip_blank_lines=False,\n)\nif input_initially_none:\nprint(\"---End of transactions---\")\ndf = format.post_processing(df)\n# convert first column to datetime and replace any invalid values with NaT\ndf[df.columns[0]] = pd.to_datetime(df[df.columns[0]], errors=\"coerce\")\ndf = df_drop_na(df)\ndf.columns = df.columns.str.strip()\n# order columns to match `MONTH_COLUMNS`\ndf = df[format.get_usecols_names()]\n# rename columns to match `MONTH_COLUMNS`\ndf = df.set_axis([c.name for c in MONTH_COLUMNS], axis=\"columns\")\n# sort rows by date\ndf = df.sort_values(by=list(df.columns), ascending=True)\n# strip whitespace from descriptions\ndf[\"Description\"] = df[\"Description\"].str.strip()\n# drop ignored transactions\ndf = df_drop_ignores(df, \"|\".join(format.ignores))\n# TODO: write issues to make ignoring identical transactions interactive\n# TODO: investigate autocompletions\nif df.duplicated().any():\nlogger.warning(\n\"The following transactions are identical:\\n\"\nf\"{df[df.duplicated(keep=False)]}\"\n)\nreturn df\n
"},{"location":"reference/xlbudget/rwxlb/","title":"rwxlb","text":"

xlbudget file reading and writing.

"},{"location":"reference/xlbudget/rwxlb/#xlbudget.rwxlb-classes","title":"Classes","text":""},{"location":"reference/xlbudget/rwxlb/#xlbudget.rwxlb.TablePosition","title":"TablePosition","text":"

The state and bounds of a worksheet table. Read-only fields were implemented with properties that return mangled variables.

Source code in src/xlbudget/rwxlb.py
class TablePosition:\n\"\"\"The state and bounds of a worksheet table.\n    Read-only fields were implemented with properties that return mangled variables.\n    \"\"\"\ndef __init__(self, ref: str) -> None:\n# excel ref format: \"<top left cell coordinate>:<bottom right cell coordinate>\"\nstart, end = ref.split(\":\")\nself.__first_col, self.__header_row = coordinate_from_string(start)\nself.next_row = self.__header_row + 1\nself.__first_col_ind = column_index_from_string(self.__first_col)\nself.__last_col, self.__initial_last_row = coordinate_from_string(end)\n@property\ndef header_row(self) -> int:\nreturn self.__header_row\n@property\ndef first_col(self) -> int:\nreturn self.__first_col_ind\n@property\ndef initial_last_row(self) -> int:\nreturn self.__initial_last_row\ndef __repr__(self) -> str:\nreturn (\nf\"{self.__class__.__name__}(next_row={self.next_row}, \"\nf\"first_col={self.first_col}, initial_last_row={self.initial_last_row}, \"\nf\"header_row={self.header_row})\"\n)\ndef get_ref(self) -> str:\n# Excel tables must have at least 2 rows: 1 header and 1+ data. `last_row` is\n# implemented as follows so that `next_row` can be incremented consistently.\nlast_row = (\nself.next_row - 1\nif self.next_row - 1 >= self.header_row + 1\nelse self.header_row + 1\n)\nreturn f\"{self.__first_col}{self.header_row}:{self.__last_col}{last_row}\"\n
"},{"location":"reference/xlbudget/rwxlb/#xlbudget.rwxlb-functions","title":"Functions","text":""},{"location":"reference/xlbudget/rwxlb/#xlbudget.rwxlb.create_year_sheet","title":"create_year_sheet","text":"
create_year_sheet(wb, year)\n

Creates a year sheet, with a table for each month.

Parameters:

Name Type Description Default wb openpyxl.workbook.workbook.Workbook

The workbook to create the sheet in.

required year int

The year.

required

Raises:

Type Description ValueError

If year sheet year already exists in the workbook wb.

Source code in src/xlbudget/rwxlb.py
def create_year_sheet(wb: Workbook, year: int) -> None:\n\"\"\"Creates a year sheet, with a table for each month.\n    Args:\n        wb (openpyxl.workbook.workbook.Workbook): The workbook to create the sheet in.\n        year (int): The year.\n    Raises:\n        ValueError: If year sheet `year` already exists in the workbook `wb`.\n    \"\"\"\nindex = 0\nyear_str = str(year)\nif year_str in wb.sheetnames:\nraise ValueError(f\"Year sheet {year_str} already exists\")\nlogger.debug(f\"Creating sheet {year_str} at {index=}\")\nws = wb.create_sheet(year_str, index)\nnum_tables = len(MONTH_NAME_0_IND)\nfor c_start in range(\nMONTH_TABLES_COL,\n(len(MONTH_COLUMNS) + 1) * num_tables + MONTH_TABLES_COL,\nlen(MONTH_COLUMNS) + 1,\n):\nmonth_ind = (c_start - MONTH_TABLES_COL) // (len(MONTH_COLUMNS) + 1)\nmonth = MONTH_NAME_0_IND[month_ind]\ntable_name = _get_month_table_name(month, year_str)\nlogger.debug(f\"creating {table_name} table\")\n_add_table(\nws, table_name, c_start, r_start=MONTH_TABLES_ROW, columns=MONTH_COLUMNS\n)\nlogger.debug(\"Creating summary table\")\nsumm_table_name = _get_summary_table_name(year_str)\n_add_table(ws, summ_table_name, c_start=1, r_start=1, columns=SUMMARY_COLUMNS)\nsumm_tab = ws.tables[summ_table_name]\nsumm_tab_pos = TablePosition(ref=summ_tab.ref)\nfor month in MONTH_NAME_0_IND:\nmonth_table_name = _get_month_table_name(month, year_str)\ntable_range = f\"{month_table_name}[{MONTH_COLUMNS[-1].name}]\"\n# set month cell\nws.cell(row=summ_tab_pos.next_row, column=summ_tab_pos.first_col).value = month\n# set incomes cell\nincomes_cell = ws.cell(\nrow=summ_tab_pos.next_row, column=summ_tab_pos.first_col + 1\n)\nincomes_cell.value = f'=SUMIFS({table_range}, {table_range}, \">0\")'\nincomes_cell.number_format = SUMMARY_COLUMNS[1].format\n# set expenses cell\nexpenses_cell = ws.cell(\nrow=summ_tab_pos.next_row, column=summ_tab_pos.first_col + 2\n)\nexpenses_cell.value = f'=-SUMIFS({table_range}, {table_range}, \"<=0\")'\nexpenses_cell.number_format = SUMMARY_COLUMNS[2].format\n# set net cell\nnet_cell = ws.cell(row=summ_tab_pos.next_row, column=summ_tab_pos.first_col + 3)\nnet_cell.value = f\"={incomes_cell.coordinate}-{expenses_cell.coordinate}\"\nnet_cell.number_format = SUMMARY_COLUMNS[3].format\nsumm_tab_pos.next_row += 1\nsumm_tab.ref = summ_tab_pos.get_ref()\n# compute totals\n# set month cell\nws.cell(row=summ_tab_pos.next_row, column=summ_tab_pos.first_col).value = \"Total\"\n# set other cells and create charts\nfor i in range(1, len(SUMMARY_COLUMNS)):\ncell = ws.cell(row=summ_tab_pos.next_row, column=summ_tab_pos.first_col + i)\ncell.value = f\"=SUM({summ_table_name}[{SUMMARY_COLUMNS[i].name}])\"\ncell.number_format = SUMMARY_COLUMNS[i].format\nchart = BarChart()\ndata = Reference(\nws,\nmin_col=i + 1,\nmin_row=summ_tab_pos.header_row,\nmax_row=summ_tab_pos.next_row - 1,\n)\ncats = Reference(\nws,\nmin_col=summ_tab_pos.first_col,\nmin_row=summ_tab_pos.header_row + 1,\nmax_row=summ_tab_pos.next_row - 1,\n)\nchart.add_data(data, titles_from_data=True)\nchart.set_categories(cats)\nchart.legend = None\nchart.y_axis.numFmt = FORMAT_NUMBER\nchart.height = 7.5\nchart.width = 8.5  # type: ignore[assignment]\nstart_col = MONTH_TABLES_COL + (i - 1) * (len(MONTH_COLUMNS) + 1)\nanchor = f\"{get_column_letter(start_col)}1\"\nws.add_chart(chart, anchor)\n
"},{"location":"reference/xlbudget/rwxlb/#xlbudget.rwxlb.df_drop_duplicates","title":"df_drop_duplicates","text":"
df_drop_duplicates(df)\n

Checks for duplicate rows, dropping them in place if any.

Parameters:

Name Type Description Default df pd.DataFrame

The original dataframe.

required

Returns:

Type Description pd.DataFrame

A[n] pd.DataFrame without any duplicate rows.

Source code in src/xlbudget/rwxlb.py
def df_drop_duplicates(df: pd.DataFrame) -> pd.DataFrame:\n\"\"\"Checks for duplicate rows, dropping them in place if any.\n    Args:\n        df (pd.DataFrame): The original dataframe.\n    Returns:\n        A[n] `pd.DataFrame` without any duplicate rows.\n    \"\"\"\nduplicated = df.duplicated()\nduplicates = df[duplicated]\nif not duplicates.empty:\nlogger.warning(f\"Dropping duplicate transactions:\\n{duplicates}\")\nreturn df[~duplicated]\nreturn df\n
"},{"location":"reference/xlbudget/rwxlb/#xlbudget.rwxlb.df_drop_ignores","title":"df_drop_ignores","text":"
df_drop_ignores(df, ignore)\n

Checks for rows containing ignore, dropping them in place if any.

Parameters:

Name Type Description Default df pd.DataFrame

The original dataframe.

required ignore str

The regex pattern that is in descriptions to ignore.

required

Returns:

Type Description pd.DataFrame

A[n] pd.DataFrame without any rows containing ignore.

Source code in src/xlbudget/rwxlb.py
def df_drop_ignores(df: pd.DataFrame, ignore: str) -> pd.DataFrame:\n\"\"\"Checks for rows containing `ignore`, dropping them in place if any.\n    Args:\n        df (pd.DataFrame): The original dataframe.\n        ignore (str): The regex pattern that is in descriptions to ignore.\n    Returns:\n        A[n] `pd.DataFrame` without any rows containing `ignore`.\n    \"\"\"\nignored = df[\"Description\"].str.contains(ignore)\nignores = df[ignored]\nif not ignores.empty:\nlogger.warning(f\"Dropping ignored transactions:\\n{ignores}\")\nreturn df[~ignored].reset_index(drop=True)\nreturn df\n
"},{"location":"reference/xlbudget/rwxlb/#xlbudget.rwxlb.df_drop_na","title":"df_drop_na","text":"
df_drop_na(df)\n

Checks for rows that contain only na values, dropping them in place if any.

Parameters:

Name Type Description Default df pd.DataFrame

The original dataframe.

required

Returns:

Type Description pd.DataFrame

A[n] pd.DataFrame without any rows that are entirely na.

Source code in src/xlbudget/rwxlb.py
def df_drop_na(df: pd.DataFrame) -> pd.DataFrame:\n\"\"\"Checks for rows that contain only `na` values, dropping them in place if any.\n    Args:\n        df (pd.DataFrame): The original dataframe.\n    Returns:\n        A[n] `pd.DataFrame` without any rows that are entirely `na`.\n    \"\"\"\nna = df.isna().all(axis=1)\nnas = df[na]\nif not nas.empty:\nlogger.info(f\"Dropping rows that contain only `na` values:\\n{nas}\")\nreturn df[~na].reset_index(drop=True)\nreturn df\n
"},{"location":"reference/xlbudget/rwxlb/#xlbudget.rwxlb.update_xlbudget","title":"update_xlbudget","text":"
update_xlbudget(wb, df)\n

Updates an xlbudget file.

Parameters:

Name Type Description Default wb openpyxl.workbook.workbook.Workbook

The xlbudget workbook.

required df pd.DataFrame

The input file dataframe.

required Source code in src/xlbudget/rwxlb.py
def update_xlbudget(wb: Workbook, df: pd.DataFrame):\n\"\"\"Updates an xlbudget file.\n    Args:\n        wb (openpyxl.workbook.workbook.Workbook): The xlbudget workbook.\n        df (pd.DataFrame): The input file dataframe.\n    \"\"\"\noldest_date, newest_date = df[df.columns[0]].agg([\"min\", \"max\"])\nlogger.debug(f\"{oldest_date=}, {newest_date=}\")\n# create year sheets as needed\nfor year in range(oldest_date.year, newest_date.year + 1):\nif str(year) not in wb.sheetnames:\nlogger.info(f\"Creating {year} sheet\")\ncreate_year_sheet(wb, year)\n# initialize table positions dictionary\n# maps worksheet names to dictionaries that map table names to their position.\ntable_pos: Dict[str, Dict[str, TablePosition]] = {}\nfor year in range(oldest_date.year, newest_date.year + 1):\nsheet_name = str(year)\ntable_pos[sheet_name] = {}\nstart_month = oldest_date.month if year == oldest_date.year else 1\nend_month = newest_date.month if year == newest_date.year else 12\nfor month in range(start_month, end_month + 1):\nmonth_name = calendar.month_name[month]\ntable_name = _get_month_table_name(month=month_name, year=sheet_name)\nlogger.debug(f\"Initializing table {table_name} in sheet {sheet_name}\")\nref = wb[sheet_name].tables[table_name].ref\ntable_pos[sheet_name][table_name] = TablePosition(ref)\n# update df with transactions in wb\nlogger.debug(f\"{df.shape=} before checking existing transactions\")\nfor sheet_name in table_pos.keys():\nws = wb[sheet_name]\nfor pos in table_pos[sheet_name].values():\nis_populated = bool(ws.cell(row=pos.next_row, column=pos.first_col).value)\nif is_populated:\nfor r in range(pos.next_row, pos.initial_last_row + 1):\ntransaction = []\nfor i in range(len(MONTH_COLUMNS)):\nc = pos.first_col + i\ntransaction.append(ws.cell(row=r, column=c).value)\nlogger.debug(f\"Appending {transaction=} to dataframe\")\n# ignore mypy error and implicitly cast to df.dtypes\ndf.loc[len(df) + 1] = transaction  # type: ignore[call-overload]\ndf = df_drop_duplicates(df)\n# re-sort transactions to make the oldest transactions come first\ndf = df.sort_values(by=list(df.columns), ascending=True)\nlogger.debug(f\"{df.shape=} after checking existing transactions\")\n# write dataframe to wb\nfor row in df.itertuples(index=False):\nlogger.debug(f\"Writing transaction {row} to workbook\")\n# get worksheet and table position\nsheet_name, month_name = str(row.Date.year), calendar.month_name[row.Date.month]\ntable_name = _get_month_table_name(month=month_name, year=sheet_name)\nws, pos = wb[sheet_name], table_pos[sheet_name][table_name]\n# set date cell\ndate_cell = ws.cell(row=pos.next_row, column=pos.first_col)\ndate_cell.value = row.Date\ndate_cell.number_format = MONTH_COLUMNS[0].format\n# set description cell\nws.cell(row=pos.next_row, column=pos.first_col + 1).value = row.Description\n# set amount cell\namount_cell = ws.cell(row=pos.next_row, column=pos.first_col + 2)\namount_cell.value = row.Amount\namount_cell.number_format = MONTH_COLUMNS[2].format\npos.next_row += 1\n# update table refs\nfor sheet_name in table_pos.keys():\nfor table_name, pos in table_pos[sheet_name].items():\ntab = wb[sheet_name].tables[table_name]\nref = pos.get_ref()\nif ref != tab.ref:\nlogger.debug(\nf\"Updating ref of table {tab.name} from {tab.ref} to {ref}\"\n)\ntab.ref = pos.get_ref()\n
"},{"location":"user_guide/commands/","title":"Commands","text":""},{"location":"user_guide/configuration/","title":"Configuration","text":""},{"location":"user_guide/getting_started/","title":"Getting Started","text":""},{"location":"user_guide/installation/","title":"Installation","text":""}]} \ No newline at end of file diff --git a/pr-24/sitemap.xml b/pr-24/sitemap.xml deleted file mode 100644 index 64512c0..0000000 --- a/pr-24/sitemap.xml +++ /dev/null @@ -1,88 +0,0 @@ - - - - https://patrick-5546.github.io/xlbudget/pr-24/ - 2024-01-05 - daily - - - https://patrick-5546.github.io/xlbudget/pr-24/coverage/ - 2024-01-05 - daily - - - https://patrick-5546.github.io/xlbudget/pr-24/license/ - 2024-01-05 - daily - - - https://patrick-5546.github.io/xlbudget/pr-24/developer_guide/contributing/ - 2024-01-05 - daily - - - https://patrick-5546.github.io/xlbudget/pr-24/developer_guide/docs/ - 2024-01-05 - daily - - - https://patrick-5546.github.io/xlbudget/pr-24/developer_guide/github_actions/ - 2024-01-05 - daily - - - https://patrick-5546.github.io/xlbudget/pr-24/developer_guide/releases/ - 2024-01-05 - daily - - - https://patrick-5546.github.io/xlbudget/pr-24/developer_guide/vscode/ - 2024-01-05 - daily - - - https://patrick-5546.github.io/xlbudget/pr-24/reference/xlbudget/ - 2024-01-05 - daily - - - https://patrick-5546.github.io/xlbudget/pr-24/reference/xlbudget/commands/ - 2024-01-05 - daily - - - https://patrick-5546.github.io/xlbudget/pr-24/reference/xlbudget/configure/ - 2024-01-05 - daily - - - https://patrick-5546.github.io/xlbudget/pr-24/reference/xlbudget/inputformat/ - 2024-01-05 - daily - - - https://patrick-5546.github.io/xlbudget/pr-24/reference/xlbudget/rwxlb/ - 2024-01-05 - daily - - - https://patrick-5546.github.io/xlbudget/pr-24/user_guide/commands/ - 2024-01-05 - daily - - - https://patrick-5546.github.io/xlbudget/pr-24/user_guide/configuration/ - 2024-01-05 - daily - - - https://patrick-5546.github.io/xlbudget/pr-24/user_guide/getting_started/ - 2024-01-05 - daily - - - https://patrick-5546.github.io/xlbudget/pr-24/user_guide/installation/ - 2024-01-05 - daily - - \ No newline at end of file diff --git a/pr-24/sitemap.xml.gz b/pr-24/sitemap.xml.gz deleted file mode 100644 index 4f8b24fcecf88a55e6d31dfa23aeaaf3956f3d64..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 369 zcmV-%0gnD3iwFq%vX^B7|8r?{Wo=<_E_iKh0M(blZo?o9K=1vEDEEZ4YNu^V(%ZgZ zJ69AOf<_?2&~$%4r>VB=JZTbK0As9YFc|9NbuiflJZfi~d|Os|239z!t!nbO*O%fk zKen4XdV>%$QnuLSeF)<-W79O1r~?dq#L{`iQ1sM6b{Dd$e9GHRMrAgzv~Hy~PPDTX zBr_uqUW@nQaM(YTN{2q4OYPXzoTF3_*yxMhp4Dugvo^so3{JMYYPT<{ZBZRa>KXyn8MLwas{dI8ZE$LGPn{hVB84)q@tEU%WY}eIR=W? zRDF<6V5;nk;)#nu_eZi`crd^blI6c3-Pp;TVGkX6N;2>>kS@7!!@#Y?q+PI`R?(*% zK-!Kmbj}YvA=fNv`V0U75!toG diff --git a/pr-24/user_guide/commands/index.html b/pr-24/user_guide/commands/index.html deleted file mode 100644 index a7714bb..0000000 --- a/pr-24/user_guide/commands/index.html +++ /dev/null @@ -1,939 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - Commands - Xlbudget Docs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - - -
- - - - - - - -
- -
- - - - -
-
- - - -
-
-
- - - - - - -
-
-
- - - -
-
-
- - - -
-
-
- - - -
-
- - - - - - - - - - - - - - - - - - - - -

Commands

- -
-
- - - Last update: - March 19, 2023 - - - -
- - - - - - -
-
- - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pr-24/user_guide/configuration/index.html b/pr-24/user_guide/configuration/index.html deleted file mode 100644 index 2fad661..0000000 --- a/pr-24/user_guide/configuration/index.html +++ /dev/null @@ -1,939 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - Configuration - Xlbudget Docs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - - -
- - - - - - - -
- -
- - - - -
-
- - - -
-
-
- - - - - - -
-
-
- - - -
-
-
- - - -
-
-
- - - -
-
- - - - - - - - - - - - - - - - - - - - -

Configuration

- -
-
- - - Last update: - March 19, 2023 - - - -
- - - - - - -
-
- - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pr-24/user_guide/getting_started/index.html b/pr-24/user_guide/getting_started/index.html deleted file mode 100644 index 77ee401..0000000 --- a/pr-24/user_guide/getting_started/index.html +++ /dev/null @@ -1,939 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - Getting Started - Xlbudget Docs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - - -
- - - - - - - -
- -
- - - - -
-
- - - -
-
-
- - - - - - -
-
-
- - - -
-
-
- - - -
-
-
- - - -
-
- - - - - - - - - - - - - - - - - - - - -

Getting Started

- -
-
- - - Last update: - March 19, 2023 - - - -
- - - - - - -
-
- - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pr-24/user_guide/installation/index.html b/pr-24/user_guide/installation/index.html deleted file mode 100644 index 2e9c4d6..0000000 --- a/pr-24/user_guide/installation/index.html +++ /dev/null @@ -1,939 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - Installation - Xlbudget Docs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - - -
- - - - - - - -
- -
- - - - -
-
- - - -
-
-
- - - - - - -
-
-
- - - -
-
-
- - - -
-
-
- - - -
-
- - - - - - - - - - - - - - - - - - - - -

Installation

- -
-
- - - Last update: - March 19, 2023 - - - -
- - - - - - -
-
- - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - - - \ No newline at end of file diff --git a/versions.json b/versions.json index df23250..221a221 100644 --- a/versions.json +++ b/versions.json @@ -1 +1 @@ -[{"version": "pr-24", "title": "pr-24", "aliases": []}, {"version": "main", "title": "main", "aliases": ["latest"]}] \ No newline at end of file +[{"version": "main", "title": "main", "aliases": ["latest"]}] \ No newline at end of file