From 1d9d0c411333fc07599c68c9bd9cd6f25b2ff9c7 Mon Sep 17 00:00:00 2001 From: ci-bot Date: Sat, 23 Dec 2023 21:11:07 +0000 Subject: [PATCH] Removed pr-13 with mike 1.1.2 --- pr-13/404.html | 841 --- pr-13/assets/_mkdocstrings.css | 36 - pr-13/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-13/assets/javascripts/lunr/tinyseg.js | 206 - pr-13/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-13/coverage/.gitignore | 2 - pr-13/coverage/coverage_html.js | 624 -- pr-13/coverage/covindex.html | 146 - .../d_ebaf54d0d3802af7___init___py.html | 109 - .../d_ebaf54d0d3802af7_commands_py.html | 406 - .../d_ebaf54d0d3802af7_configure_py.html | 196 - .../d_ebaf54d0d3802af7_inputformat_py.html | 219 - .../coverage/d_ebaf54d0d3802af7_rwxlb_py.html | 391 - pr-13/coverage/favicon_32.png | Bin 1732 -> 0 bytes pr-13/coverage/index.html | 946 --- pr-13/coverage/keybd_closed.png | Bin 9004 -> 0 bytes pr-13/coverage/keybd_open.png | Bin 9003 -> 0 bytes pr-13/coverage/status.json | 1 - pr-13/coverage/style.css | 309 - pr-13/developer_guide/contributing/index.html | 1204 --- pr-13/developer_guide/docs/index.html | 1095 --- .../developer_guide/github_actions/index.html | 1259 ---- pr-13/developer_guide/releases/index.html | 992 --- pr-13/developer_guide/vscode/index.html | 1180 --- pr-13/gen_ref_pages.py | 32 - pr-13/index.html | 920 --- pr-13/javascripts/mathjax.js | 16 - pr-13/license/index.html | 948 --- pr-13/objects.inv | Bin 461 -> 0 bytes pr-13/overrides/main.html | 8 - pr-13/reference/SUMMARY/index.html | 859 --- pr-13/reference/xlbudget/commands/index.html | 3079 -------- pr-13/reference/xlbudget/configure/index.html | 1420 ---- pr-13/reference/xlbudget/index.html | 1007 --- .../reference/xlbudget/inputformat/index.html | 1465 ---- pr-13/reference/xlbudget/rwxlb/index.html | 1989 ----- pr-13/search/search_index.json | 1 - pr-13/sitemap.xml | 88 - pr-13/sitemap.xml.gz | Bin 370 -> 0 bytes pr-13/user_guide/commands/index.html | 939 --- pr-13/user_guide/configuration/index.html | 939 --- pr-13/user_guide/getting_started/index.html | 939 --- pr-13/user_guide/installation/index.html | 939 --- versions.json | 2 +- 82 files changed, 1 insertion(+), 32835 deletions(-) delete mode 100644 pr-13/404.html delete mode 100644 pr-13/assets/_mkdocstrings.css delete mode 100644 pr-13/assets/images/favicon.png delete mode 100644 pr-13/assets/javascripts/bundle.fac441b0.min.js delete mode 100644 pr-13/assets/javascripts/bundle.fac441b0.min.js.map delete mode 100644 pr-13/assets/javascripts/lunr/min/lunr.ar.min.js delete mode 100644 pr-13/assets/javascripts/lunr/min/lunr.da.min.js delete mode 100644 pr-13/assets/javascripts/lunr/min/lunr.de.min.js delete mode 100644 pr-13/assets/javascripts/lunr/min/lunr.du.min.js delete mode 100644 pr-13/assets/javascripts/lunr/min/lunr.es.min.js delete mode 100644 pr-13/assets/javascripts/lunr/min/lunr.fi.min.js delete mode 100644 pr-13/assets/javascripts/lunr/min/lunr.fr.min.js delete mode 100644 pr-13/assets/javascripts/lunr/min/lunr.hi.min.js delete mode 100644 pr-13/assets/javascripts/lunr/min/lunr.hu.min.js delete mode 100644 pr-13/assets/javascripts/lunr/min/lunr.hy.min.js delete mode 100644 pr-13/assets/javascripts/lunr/min/lunr.it.min.js delete mode 100644 pr-13/assets/javascripts/lunr/min/lunr.ja.min.js delete mode 100644 pr-13/assets/javascripts/lunr/min/lunr.jp.min.js delete mode 100644 pr-13/assets/javascripts/lunr/min/lunr.kn.min.js delete mode 100644 pr-13/assets/javascripts/lunr/min/lunr.ko.min.js delete mode 100644 pr-13/assets/javascripts/lunr/min/lunr.multi.min.js delete mode 100644 pr-13/assets/javascripts/lunr/min/lunr.nl.min.js delete mode 100644 pr-13/assets/javascripts/lunr/min/lunr.no.min.js delete mode 100644 pr-13/assets/javascripts/lunr/min/lunr.pt.min.js delete mode 100644 pr-13/assets/javascripts/lunr/min/lunr.ro.min.js delete mode 100644 pr-13/assets/javascripts/lunr/min/lunr.ru.min.js delete mode 100644 pr-13/assets/javascripts/lunr/min/lunr.sa.min.js delete mode 100644 pr-13/assets/javascripts/lunr/min/lunr.stemmer.support.min.js delete mode 100644 pr-13/assets/javascripts/lunr/min/lunr.sv.min.js delete mode 100644 pr-13/assets/javascripts/lunr/min/lunr.ta.min.js delete mode 100644 pr-13/assets/javascripts/lunr/min/lunr.te.min.js delete mode 100644 pr-13/assets/javascripts/lunr/min/lunr.th.min.js delete mode 100644 pr-13/assets/javascripts/lunr/min/lunr.tr.min.js delete mode 100644 pr-13/assets/javascripts/lunr/min/lunr.vi.min.js delete mode 100644 pr-13/assets/javascripts/lunr/min/lunr.zh.min.js delete mode 100644 pr-13/assets/javascripts/lunr/tinyseg.js delete mode 100644 pr-13/assets/javascripts/lunr/wordcut.js delete mode 100644 pr-13/assets/javascripts/workers/search.208ed371.min.js delete mode 100644 pr-13/assets/javascripts/workers/search.208ed371.min.js.map delete mode 100644 pr-13/assets/stylesheets/main.85bb2934.min.css delete mode 100644 pr-13/assets/stylesheets/main.85bb2934.min.css.map delete mode 100644 pr-13/assets/stylesheets/palette.a6bdf11c.min.css delete mode 100644 pr-13/assets/stylesheets/palette.a6bdf11c.min.css.map delete mode 100644 pr-13/coverage/.gitignore delete mode 100644 pr-13/coverage/coverage_html.js delete mode 100644 pr-13/coverage/covindex.html delete mode 100644 pr-13/coverage/d_ebaf54d0d3802af7___init___py.html delete mode 100644 pr-13/coverage/d_ebaf54d0d3802af7_commands_py.html delete mode 100644 pr-13/coverage/d_ebaf54d0d3802af7_configure_py.html delete mode 100644 pr-13/coverage/d_ebaf54d0d3802af7_inputformat_py.html delete mode 100644 pr-13/coverage/d_ebaf54d0d3802af7_rwxlb_py.html delete mode 100644 pr-13/coverage/favicon_32.png delete mode 100644 pr-13/coverage/index.html delete mode 100644 pr-13/coverage/keybd_closed.png delete mode 100644 pr-13/coverage/keybd_open.png delete mode 100644 pr-13/coverage/status.json delete mode 100644 pr-13/coverage/style.css delete mode 100644 pr-13/developer_guide/contributing/index.html delete mode 100644 pr-13/developer_guide/docs/index.html delete mode 100644 pr-13/developer_guide/github_actions/index.html delete mode 100644 pr-13/developer_guide/releases/index.html delete mode 100644 pr-13/developer_guide/vscode/index.html delete mode 100644 pr-13/gen_ref_pages.py delete mode 100644 pr-13/index.html delete mode 100644 pr-13/javascripts/mathjax.js delete mode 100644 pr-13/license/index.html delete mode 100644 pr-13/objects.inv delete mode 100644 pr-13/overrides/main.html delete mode 100644 pr-13/reference/SUMMARY/index.html delete mode 100644 pr-13/reference/xlbudget/commands/index.html delete mode 100644 pr-13/reference/xlbudget/configure/index.html delete mode 100644 pr-13/reference/xlbudget/index.html delete mode 100644 pr-13/reference/xlbudget/inputformat/index.html delete mode 100644 pr-13/reference/xlbudget/rwxlb/index.html delete mode 100644 pr-13/search/search_index.json delete mode 100644 pr-13/sitemap.xml delete mode 100644 pr-13/sitemap.xml.gz delete mode 100644 pr-13/user_guide/commands/index.html delete mode 100644 pr-13/user_guide/configuration/index.html delete mode 100644 pr-13/user_guide/getting_started/index.html delete mode 100644 pr-13/user_guide/installation/index.html diff --git a/pr-13/404.html b/pr-13/404.html deleted file mode 100644 index 1c35e9e..0000000 --- a/pr-13/404.html +++ /dev/null @@ -1,841 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - Xlbudget Docs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
- -
- - - - - - - - -
- - - - - - - -
- -
- - - - -
-
- - - -
-
-
- - - - - - -
-
-
- - - -
-
-
- - - -
-
-
- - - -
-
- -

404 - Not found

- -
-
- - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pr-13/assets/_mkdocstrings.css b/pr-13/assets/_mkdocstrings.css deleted file mode 100644 index a65078d..0000000 --- a/pr-13/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-13/assets/images/favicon.png b/pr-13/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-13/assets/javascripts/bundle.fac441b0.min.js b/pr-13/assets/javascripts/bundle.fac441b0.min.js deleted file mode 100644 index 4bb4cd6..0000000 --- a/pr-13/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: - 52.07% -

- -
- -
-

- coverage.py v7.2.5, - created at 2023-12-23 21:09 +0000 -

-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Modulestatementsmissingexcludedbranchespartialcoverage
src/xlbudget/__init__.py5300040.00%
src/xlbudget/commands.py11337016167.44%
src/xlbudget/configure.py251402040.74%
src/xlbudget/inputformat.py282010192.11%
src/xlbudget/rwxlb.py14787040035.29%
Total318143068252.07%
-

- No items found using the specified filter. -

-
- - - diff --git a/pr-13/coverage/d_ebaf54d0d3802af7___init___py.html b/pr-13/coverage/d_ebaf54d0d3802af7___init___py.html deleted file mode 100644 index 007b1af..0000000 --- a/pr-13/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-13/coverage/d_ebaf54d0d3802af7_commands_py.html b/pr-13/coverage/d_ebaf54d0d3802af7_commands_py.html deleted file mode 100644 index 474415f..0000000 --- a/pr-13/coverage/d_ebaf54d0d3802af7_commands_py.html +++ /dev/null @@ -1,406 +0,0 @@ - - - - - Coverage for src/xlbudget/commands.py: 67.44% - - - - - -
-
-

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

- -

- 113 statements   - - - - -

-

- « prev     - ^ index     - » next -       - coverage.py v7.2.5, - created at 2023-12-23 21:09 +0000 -

- -
-
-
-

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

-

2 

-

3import datetime 

-

4import os 

-

5import sys 

-

6from abc import ABC, abstractmethod 

-

7from argparse import ArgumentParser, Namespace, _SubParsersAction 

-

8from logging import getLogger 

-

9from typing import List, Type 

-

10 

-

11from openpyxl import Workbook, load_workbook 

-

12 

-

13from xlbudget.inputformat import GetInputFormats, parse_input 

-

14from xlbudget.rwxlb import create_year_sheet, update_xlbudget 

-

15 

-

16logger = getLogger(__name__) 

-

17 

-

18 

-

19class Command(ABC): 

-

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

-

21 

-

22 Attributes: Class Attributes 

-

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

-

24 

-

25 Attributes: 

-

26 trial (bool): If True, the xlbudget file will not be generated/modified. 

-

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

-

28 """ 

-

29 

-

30 default_path: str = "xlbudget.xlsx" 

-

31 

-

32 @property 

-

33 @abstractmethod 

-

34 def name(self) -> str: 

-

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

-

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

-

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

-

38 """ 

-

39 raise NotImplementedError 

-

40 

-

41 def get_name(self) -> str: 

-

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

-

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

-

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

-

45 """ 

-

46 return self.name 

-

47 

-

48 @property 

-

49 @abstractmethod 

-

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

-

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

-

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

-

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

-

54 """ 

-

55 raise NotImplementedError 

-

56 

-

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

-

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

-

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

-

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

-

61 """ 

-

62 return self.aliases 

-

63 

-

64 @classmethod 

-

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

-

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

-

67 

-

68 Args: 

-

69 parser (ArgumentParser): The argument parser. 

-

70 """ 

-

71 parser.add_argument( 

-

72 "-t", 

-

73 "--trial", 

-

74 action="store_true", 

-

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

-

76 ) 

-

77 parser.add_argument( 

-

78 "-p", 

-

79 "--path", 

-

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

-

81 default=cls.default_path, 

-

82 ) 

-

83 

-

84 @classmethod 

-

85 @abstractmethod 

-

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

-

87 pass 

-

88 

-

89 @abstractmethod 

-

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

-

91 self.trial = args.trial 

-

92 

-

93 self._check_path(args.path) 

-

94 self.path = args.path 

-

95 

-

96 @staticmethod 

-

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

-

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

-

99 

-

100 Args: 

-

101 path (str): The xlbudget path. 

-

102 

-

103 Raises: 

-

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

-

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

-

106 """ 

-

107 xlsx_ext = ".xlsx" 

-

108 if not path.endswith(xlsx_ext): 

-

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

-

110 

-

111 dir = os.path.dirname(path) 

-

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

-

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

-

114 

-

115 @abstractmethod 

-

116 def run(self) -> None: 

-

117 pass 

-

118 

-

119 

-

120class Generate(Command): 

-

121 """The `generate` command generates a new xlbudget file. 

-

122 

-

123 Attributes: Class Attributes 

-

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

-

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

-

126 

-

127 Attributes: 

-

128 force (bool): If True and file exists, will overwrite it. 

-

129 """ 

-

130 

-

131 name: str = "generate" 

-

132 aliases: List[str] = ["g"] 

-

133 

-

134 @classmethod 

-

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

-

136 """Configures the argument parser for the `generate` command. 

-

137 

-

138 Args: 

-

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

-

140 """ 

-

141 parser = _add_parser( 

-

142 subparsers, 

-

143 name=cls.name, 

-

144 aliases=cls.aliases, 

-

145 help="generate a new xlbudget file", 

-

146 cmd_cls=Generate, 

-

147 ) 

-

148 

-

149 parser.add_argument( 

-

150 "-f", "--force", action="store_true", help="overwrite file if it exists" 

-

151 ) 

-

152 

-

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

-

154 super().__init__(args) 

-

155 

-

156 if not args.force and os.path.exists(self.path): 156 ↛ 157line 156 didn't jump to line 157, because the condition on line 156 was never true

-

157 raise FileExistsError( 

-

158 f"File {self.path} exists, run with -f/--force to overwrite" 

-

159 ) 

-

160 

-

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

-

162 

-

163 def run(self) -> None: 

-

164 """Creates an empty xlbudget file populated with: 

-

165 

-

166 - A sheet for the current year. 

-

167 

-

168 Raises: 

-

169 FileExistsError: If `self.force` is false and the file exists. 

-

170 """ 

-

171 # create workbook without any sheets 

-

172 wb = Workbook() 

-

173 ws = wb.active 

-

174 # ignore type mismatch of active worksheet 

-

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

-

176 

-

177 year = datetime.date.today().year 

-

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

-

179 create_year_sheet(wb, year) 

-

180 

-

181 if not self.trial: 

-

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

-

183 wb.save(self.path) 

-

184 else: 

-

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

-

186 

-

187 

-

188class Update(Command): 

-

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

-

190 

-

191 Attributes: Class Attributes 

-

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

-

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

-

194 

-

195 Attributes: 

-

196 input (str): The path to the input file. 

-

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

-

198 """ 

-

199 

-

200 name: str = "update" 

-

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

-

202 

-

203 @classmethod 

-

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

-

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

-

206 

-

207 Args: 

-

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

-

209 """ 

-

210 parser = _add_parser( 

-

211 subparsers, 

-

212 name=cls.name, 

-

213 aliases=cls.aliases, 

-

214 help="update an existing xlbudget file", 

-

215 cmd_cls=Update, 

-

216 ) 

-

217 

-

218 parser.add_argument("input", help="path to the input file") 

-

219 parser.add_argument( 

-

220 "format", 

-

221 action=GetInputFormats, 

-

222 choices=GetInputFormats.input_formats.keys(), 

-

223 help="select an input file format", 

-

224 ) 

-

225 

-

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

-

227 super().__init__(args) 

-

228 

-

229 self._check_input(args.input) 

-

230 self.input = args.input 

-

231 self.format = args.format 

-

232 

-

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

-

234 

-

235 @staticmethod 

-

236 def _check_input(input: str) -> None: 

-

237 """Check that `input` is a valid path to an input file. 

-

238 

-

239 Args: 

-

240 input (str): The input path. 

-

241 

-

242 Raises: 

-

243 ValueError: If `input` is not a CSV file. 

-

244 ValueError: If `input` is not an existing file. 

-

245 """ 

-

246 csv_ext = ".csv" 

-

247 if not input.endswith(csv_ext): 

-

248 raise ValueError(f"Input '{input}' does not end with '{csv_ext}'") 

-

249 

-

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

-

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

-

252 

-

253 def run(self) -> None: 

-

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

-

255 df = parse_input(self.input, self.format) 

-

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

-

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

-

258 

-

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

-

260 wb = load_workbook(self.path) 

-

261 

-

262 logger.info("Updating xlbudget file") 

-

263 update_xlbudget(wb, df) 

-

264 

-

265 if not self.trial: 

-

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

-

267 wb.save(self.path) 

-

268 else: 

-

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

-

270 

-

271 

-

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

-

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

-

274 

-

275 Returns: 

-

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

-

277 """ 

-

278 command_module = sys.modules[__name__] 

-

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

-

280 

-

281 

-

282def _add_parser( 

-

283 subparsers: _SubParsersAction, 

-

284 name: str, 

-

285 aliases: List[str], 

-

286 help: str, 

-

287 cmd_cls: Type[Command], 

-

288) -> ArgumentParser: 

-

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

-

290 across commands should go here. 

-

291 

-

292 Args: 

-

293 subparsers (_SubParsersAction): The subparsers object. 

-

294 name (str): The command name. 

-

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

-

296 help (str): The command help message. 

-

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

-

298 

-

299 Returns: 

-

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

-

301 """ 

-

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

-

303 

-

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

-

305 parser.set_defaults(init=cmd_cls) 

-

306 

-

307 return parser 

-
- - - diff --git a/pr-13/coverage/d_ebaf54d0d3802af7_configure_py.html b/pr-13/coverage/d_ebaf54d0d3802af7_configure_py.html deleted file mode 100644 index 416fd4b..0000000 --- a/pr-13/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 2023-12-23 21:09 +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-13/coverage/d_ebaf54d0d3802af7_inputformat_py.html b/pr-13/coverage/d_ebaf54d0d3802af7_inputformat_py.html deleted file mode 100644 index b7cef61..0000000 --- a/pr-13/coverage/d_ebaf54d0d3802af7_inputformat_py.html +++ /dev/null @@ -1,219 +0,0 @@ - - - - - Coverage for src/xlbudget/inputformat.py: 92.11% - - - - - -
-
-

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

- -

- 28 statements   - - - - -

-

- « prev     - ^ index     - » next -       - coverage.py v7.2.5, - created at 2023-12-23 21:09 +0000 -

- -
-
-
-

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

-

2 

-

3from argparse import Action 

-

4from typing import Dict, List, NamedTuple 

-

5 

-

6import pandas as pd 

-

7 

-

8from xlbudget.rwxlb import COLUMNS, df_drop_ignores, df_drop_na 

-

9 

-

10 

-

11class InputFormat(NamedTuple): 

-

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

-

13 

-

14 Attributes: 

-

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

-

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

-

17 usecols (List[int]): The indices of columns that map to `COLUMNS`. 

-

18 ignores (List[str]): Ignore transactions that start with these strings. 

-

19 """ 

-

20 

-

21 header: int 

-

22 names: List[str] 

-

23 usecols: List[int] 

-

24 ignores: List[str] 

-

25 

-

26 def get_usecols_names(self): 

-

27 return [self.names[i] for i in self.usecols] 

-

28 

-

29 

-

30# define input formats below 

-

31 

-

32BMO_ACCT = InputFormat( 

-

33 header=3, 

-

34 names=[ 

-

35 "First Bank Card", 

-

36 "Transaction Type", 

-

37 "Date Posted", 

-

38 "Transaction Amount", 

-

39 "Description", 

-

40 ], 

-

41 usecols=[2, 4, 3], 

-

42 ignores=["[CW] TF"], 

-

43) 

-

44 

-

45BMO_CC = InputFormat( 

-

46 header=2, 

-

47 names=[ 

-

48 "Item #", 

-

49 "Card #", 

-

50 "Transaction Date", 

-

51 "Posting Date", 

-

52 "Transaction Amount", 

-

53 "Description", 

-

54 ], 

-

55 usecols=[2, 5, 4], 

-

56 ignores=["TRSF FROM"], 

-

57) 

-

58 

-

59 

-

60# define input formats above 

-

61 

-

62 

-

63class GetInputFormats(Action): 

-

64 """Argparse action for the format argument. 

-

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

-

66 

-

67 Attributes: 

-

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

-

69 """ 

-

70 

-

71 input_formats: Dict[str, InputFormat] = { 

-

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

-

73 } 

-

74 

-

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

-

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

-

77 

-

78 

-

79def parse_input(path: str, format: InputFormat) -> pd.DataFrame: 

-

80 """Parses an input file. 

-

81 

-

82 Args: 

-

83 path (str): The path to the input file. 

-

84 format (InputFormat): The input file format. 

-

85 

-

86 Raises: 

-

87 ValueError: If input file contains duplicate transactions. 

-

88 

-

89 Returns: 

-

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

-

91 """ 

-

92 df = pd.read_csv( 

-

93 path, 

-

94 header=format.header, 

-

95 usecols=format.usecols, 

-

96 parse_dates=[0], 

-

97 skip_blank_lines=False, 

-

98 ) 

-

99 

-

100 df = df_drop_na(df) 

-

101 

-

102 # TODO: write issues to make ignoring duplicate transactions interactive 

-

103 # they might not be an error 

-

104 # TODO: investigate autocompletions 

-

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

-

106 raise ValueError("Input file contains duplicate transactions") 

-

107 

-

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

-

109 

-

110 # order to match `COLUMNS` 

-

111 df = df[format.get_usecols_names()] 

-

112 

-

113 # rename to match `COLUMNS` 

-

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

-

115 

-

116 # drop ignored transactions 

-

117 for ignore in format.ignores: 

-

118 df = df_drop_ignores(df, ignore) 

-

119 

-

120 return df 

-
- - - diff --git a/pr-13/coverage/d_ebaf54d0d3802af7_rwxlb_py.html b/pr-13/coverage/d_ebaf54d0d3802af7_rwxlb_py.html deleted file mode 100644 index 2eb2a5d..0000000 --- a/pr-13/coverage/d_ebaf54d0d3802af7_rwxlb_py.html +++ /dev/null @@ -1,391 +0,0 @@ - - - - - Coverage for src/xlbudget/rwxlb.py: 35.29% - - - - - -
-
-

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

- -

- 147 statements   - - - - -

-

- « prev     - ^ index     - » next -       - coverage.py v7.2.5, - created at 2023-12-23 21:09 +0000 -

- -
-
-
-

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

-

2 

-

3import calendar 

-

4from logging import getLogger 

-

5from typing import Dict, NamedTuple 

-

6 

-

7import pandas as pd 

-

8from openpyxl import Workbook 

-

9from openpyxl.utils import get_column_letter 

-

10from openpyxl.utils.cell import column_index_from_string, coordinate_from_string 

-

11from openpyxl.worksheet.table import Table, TableStyleInfo 

-

12 

-

13logger = getLogger(__name__) 

-

14 

-

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

-

16FORMAT_DATE = "MM/DD/YYYY" 

-

17 

-

18MONTH_NAME_0_IND = calendar.month_name[1:] 

-

19 

-

20 

-

21class ColumnSpecs(NamedTuple): 

-

22 name: str 

-

23 format: str 

-

24 width: int 

-

25 

-

26 

-

27COLUMNS = [ 

-

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

-

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

-

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

-

31] 

-

32 

-

33 

-

34class TablePosition: 

-

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

-

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

-

37 """ 

-

38 

-

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

-

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

-

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

-

42 

-

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

-

44 self.next_row = self.__header_row + 1 

-

45 self.__first_col_ind = column_index_from_string(self.__first_col) 

-

46 

-

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

-

48 

-

49 @property 

-

50 def first_col(self) -> int: 

-

51 return self.__first_col_ind 

-

52 

-

53 @property 

-

54 def initial_last_row(self) -> int: 

-

55 return self.__initial_last_row 

-

56 

-

57 def __repr__(self) -> str: 

-

58 return ( 

-

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

-

60 f"first_col={self.first_col}, initial_last_row={self.initial_last_row})" 

-

61 ) 

-

62 

-

63 def get_ref(self) -> str: 

-

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

-

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

-

66 last_row = ( 

-

67 self.next_row - 1 

-

68 if self.next_row - 1 >= self.__header_row + 1 

-

69 else self.__header_row + 1 

-

70 ) 

-

71 return f"{self.__first_col}{self.__header_row}:{self.__last_col}{last_row}" 

-

72 

-

73 

-

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

-

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

-

76 

-

77 Args: 

-

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

-

79 year (int): The year. 

-

80 

-

81 Raises: 

-

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

-

83 """ 

-

84 index = 0 

-

85 year_str = str(year) 

-

86 if year_str in wb.sheetnames: 

-

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

-

88 

-

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

-

90 ws = wb.create_sheet(year_str, index) 

-

91 num_tables = len(MONTH_NAME_0_IND) 

-

92 

-

93 for c_start in range(1, (len(COLUMNS) + 1) * num_tables + 1, len(COLUMNS) + 1): 

-

94 month_ind = c_start // (len(COLUMNS) + 1) 

-

95 month = MONTH_NAME_0_IND[month_ind] 

-

96 table_name = _get_table_name(month, year_str) 

-

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

-

98 

-

99 # table title 

-

100 ws.cell(row=1, column=c_start).value = month 

-

101 ws.merge_cells( 

-

102 start_row=1, 

-

103 start_column=c_start, 

-

104 end_row=1, 

-

105 end_column=c_start + len(COLUMNS) - 2, 

-

106 ) 

-

107 

-

108 # table sum 

-

109 sum = ws.cell(row=1, column=c_start + len(COLUMNS) - 1) 

-

110 sum.value = f"=SUM({table_name}[{COLUMNS[-1].name}])" 

-

111 sum.number_format = FORMAT_ACCOUNTING 

-

112 logger.debug(f"created sum cell {sum.coordinate}='{sum.value}'") 

-

113 

-

114 # table header and formating 

-

115 for i in range(len(COLUMNS)): 

-

116 c = c_start + i 

-

117 

-

118 # header 

-

119 ws.cell(row=2, column=c).value = COLUMNS[i].name 

-

120 

-

121 # column format 

-

122 cell = ws.cell(row=3, column=c) 

-

123 if COLUMNS[i].format: 

-

124 cell.number_format = COLUMNS[i].format 

-

125 

-

126 # column width 

-

127 ws.column_dimensions[get_column_letter(c)].width = COLUMNS[i].width 

-

128 

-

129 # create table 

-

130 c_start_ltr = get_column_letter(c_start) 

-

131 c_end_ltr = get_column_letter(c_start + len(COLUMNS) - 1) 

-

132 ref = f"{c_start_ltr}2:{c_end_ltr}3" 

-

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

-

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

-

135 

-

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

-

137 style = TableStyleInfo( 

-

138 name="TableStyleMedium9", 

-

139 showFirstColumn=False, 

-

140 showLastColumn=False, 

-

141 showRowStripes=True, 

-

142 showColumnStripes=True, 

-

143 ) 

-

144 tab.tableStyleInfo = style 

-

145 

-

146 ws.add_table(tab) 

-

147 

-

148 

-

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

-

150 """Updates an xlbudget file. 

-

151 

-

152 Args: 

-

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

-

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

-

155 """ 

-

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

-

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

-

158 

-

159 # create year sheets as needed 

-

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

-

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

-

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

-

163 create_year_sheet(wb, year) 

-

164 

-

165 # initialize table positions dictionary 

-

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

-

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

-

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

-

169 sheet_name = str(year) 

-

170 table_pos[sheet_name] = {} 

-

171 

-

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

-

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

-

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

-

175 month_name = calendar.month_name[month] 

-

176 table_name = _get_table_name(month=month_name, year=sheet_name) 

-

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

-

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

-

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

-

180 

-

181 # update df with transactions in wb 

-

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

-

183 for sheet_name in table_pos.keys(): 

-

184 ws = wb[sheet_name] 

-

185 

-

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

-

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

-

188 if is_populated: 

-

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

-

190 transaction = [] 

-

191 for i in range(len(COLUMNS)): 

-

192 c = pos.first_col + i 

-

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

-

194 

-

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

-

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

-

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

-

198 df = df_drop_duplicates(df) 

-

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

-

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

-

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

-

202 

-

203 # write dataframe to wb 

-

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

-

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

-

206 

-

207 # get worksheet and table position 

-

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

-

209 table_name = _get_table_name(month=month_name, year=sheet_name) 

-

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

-

211 

-

212 # set date cell 

-

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

-

214 date_cell.value = row.Date 

-

215 date_cell.number_format = FORMAT_DATE 

-

216 

-

217 # set description cell 

-

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

-

219 

-

220 # set amount cell 

-

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

-

222 amount_cell.value = row.Amount 

-

223 amount_cell.number_format = FORMAT_ACCOUNTING 

-

224 

-

225 pos.next_row += 1 

-

226 

-

227 # update table refs 

-

228 for sheet_name in table_pos.keys(): 

-

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

-

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

-

231 ref = pos.get_ref() 

-

232 if ref != tab.ref: 

-

233 logger.debug( 

-

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

-

235 ) 

-

236 tab.ref = pos.get_ref() 

-

237 

-

238 

-

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

-

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

-

241 

-

242 Args: 

-

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

-

244 

-

245 Returns: 

-

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

-

247 """ 

-

248 duplicated = df.duplicated() 

-

249 duplicates = df[duplicated] 

-

250 if not duplicates.empty: 

-

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

-

252 return df[~duplicated] 

-

253 return df 

-

254 

-

255 

-

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

-

257 """Checks for rows that start with `ignore`, dropping them in place if any. 

-

258 

-

259 Args: 

-

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

-

261 ignore (str): The string that begins descriptions to ignore. 

-

262 

-

263 Returns: 

-

264 A[n] `pd.DataFrame` without any rows that start with `ignore`. 

-

265 """ 

-

266 ignored = df["Description"].str.startswith(ignore) 

-

267 ignores = df[ignored] 

-

268 if not ignores.empty: 

-

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

-

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

-

271 return df 

-

272 

-

273 

-

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

-

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

-

276 

-

277 Args: 

-

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

-

279 

-

280 Returns: 

-

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

-

282 """ 

-

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

-

284 nas = df[na] 

-

285 if not nas.empty: 

-

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

-

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

-

288 return df 

-

289 

-

290 

-

291def _get_table_name(month: str, year: str): 

-

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

-
- - - diff --git a/pr-13/coverage/favicon_32.png b/pr-13/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-13/coverage/keybd_closed.png b/pr-13/coverage/keybd_closed.png deleted file mode 100644 index ba119c47df81ed2bbd27a06988abf700139c4f99..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9004 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 diff --git a/pr-13/coverage/keybd_open.png b/pr-13/coverage/keybd_open.png deleted file mode 100644 index a8bac6c9de256626c680f9e9e3f8ee81d9713ecd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9003 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 G%6|bpD&^7u diff --git a/pr-13/coverage/status.json b/pr-13/coverage/status.json deleted file mode 100644 index 1d6e35b..0000000 --- a/pr-13/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":"c7ed9d75335cd71e84c354f0bbdf9932","index":{"nums":[2,1,113,0,37,16,1,5],"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":"f5b67438361c474166ffd1129c53dd5d","index":{"nums":[2,1,28,0,2,10,1,1],"html_filename":"d_ebaf54d0d3802af7_inputformat_py.html","relative_filename":"src/xlbudget/inputformat.py"}},"d_ebaf54d0d3802af7_rwxlb_py":{"hash":"40016bab230ab6c29581db777462ca1a","index":{"nums":[2,1,147,0,87,40,0,34],"html_filename":"d_ebaf54d0d3802af7_rwxlb_py.html","relative_filename":"src/xlbudget/rwxlb.py"}}}} \ No newline at end of file diff --git a/pr-13/coverage/style.css b/pr-13/coverage/style.css deleted file mode 100644 index 11b24c4..0000000 --- a/pr-13/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-13/developer_guide/contributing/index.html b/pr-13/developer_guide/contributing/index.html deleted file mode 100644 index d604c8a..0000000 --- a/pr-13/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-13/developer_guide/docs/index.html b/pr-13/developer_guide/docs/index.html deleted file mode 100644 index 21ef819..0000000 --- a/pr-13/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-13/developer_guide/github_actions/index.html b/pr-13/developer_guide/github_actions/index.html deleted file mode 100644 index a5e118a..0000000 --- a/pr-13/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@v4"
-        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@v3
-        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@v4"
-        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@v4
-        with:
-          python-version: "3.10"
-          cache: pip
-          cache-dependency-path: '**/requirements/docs.txt'
-
-      - name: Download HTML coverage report
-        uses: actions/download-artifact@v3
-        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@v4
-        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@v4
-      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-13/developer_guide/releases/index.html b/pr-13/developer_guide/releases/index.html deleted file mode 100644 index eb0dbe9..0000000 --- a/pr-13/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-13/developer_guide/vscode/index.html b/pr-13/developer_guide/vscode/index.html deleted file mode 100644 index 1209588..0000000 --- a/pr-13/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-13/gen_ref_pages.py b/pr-13/gen_ref_pages.py deleted file mode 100644 index ebc17be..0000000 --- a/pr-13/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-13/index.html b/pr-13/index.html deleted file mode 100644 index e0a356c..0000000 --- a/pr-13/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-13/javascripts/mathjax.js b/pr-13/javascripts/mathjax.js deleted file mode 100644 index 080801e..0000000 --- a/pr-13/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-13/license/index.html b/pr-13/license/index.html deleted file mode 100644 index 84c8dcf..0000000 --- a/pr-13/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-13/objects.inv b/pr-13/objects.inv deleted file mode 100644 index 2ee020c1fdc6b1c5470bfce03d15e271cfcf4d84..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 461 zcmV;;0W$t0AX9K?X>NERX>N99Zgg*Qc_4OWa&u{KZXhxWBOp+6Z)#;@bUGkdY+`j} zXJvFCL~mnr3L_v^WpZMd?av*PJAarPHb0B7E zY-J#6b0A}HZE$jBb8}^6Aa!$TZf78RY-wUH3V7PJ)Uj^EAP@l1ynQ1w(XHpoa4 zH-~Z8=Ki62yqjjVG<^@GT>Wwy4Dqr9=gtRD+ZU7?It>oppn>7Lg>@ubrq1afVg6*`7t}CJGmJFAn2y&E=ep+H$y@~n(R?JG8 Do9Nbd diff --git a/pr-13/overrides/main.html b/pr-13/overrides/main.html deleted file mode 100644 index 8d9f505..0000000 --- a/pr-13/overrides/main.html +++ /dev/null @@ -1,8 +0,0 @@ -{% extends "base.html" %} - -{% block outdated %} - You're not viewing the latest version. - - Click here to go to latest. - -{% endblock %} diff --git a/pr-13/reference/SUMMARY/index.html b/pr-13/reference/SUMMARY/index.html deleted file mode 100644 index b4aac1a..0000000 --- a/pr-13/reference/SUMMARY/index.html +++ /dev/null @@ -1,859 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - SUMMARY - Xlbudget Docs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
- -
- - - - - - - - -
- - - - - - - -
- -
- - - - -
-
- - - -
-
-
- - - - - - -
-
-
- - - -
-
-
- - - -
-
-
- - - -
- -
- - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pr-13/reference/xlbudget/commands/index.html b/pr-13/reference/xlbudget/commands/index.html deleted file mode 100644 index 578410d..0000000 --- a/pr-13/reference/xlbudget/commands/index.html +++ /dev/null @@ -1,3079 +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 generated/modified.

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 generated/modified.
-        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 -
64
-65
-66
-67
-68
-69
-70
-71
-72
-73
-74
-75
-76
-77
-78
-79
-80
-81
-82
@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 -
57
-58
-59
-60
-61
-62
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 -
41
-42
-43
-44
-45
-46
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
-
-
-
- -
- - - -
- -
- -
- -
- - - -

- Generate - - -

- - -
-

- Bases: Command

- - -

The generate command generates a new xlbudget file.

- -

Class Attributes

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

The command's CLI name.

aliases - List[str] -

The command's CLI aliases.

- -

Attributes:

- - - - - - - - - - - - - - - -
NameTypeDescription
force - bool -

If True and file exists, will overwrite it.

- - -
- Source code in src/xlbudget/commands.py -
class Generate(Command):
-    """The `generate` command generates a new xlbudget file.
-
-    Attributes: Class Attributes
-        name (str): The command's CLI name.
-        aliases (List[str]): The command's CLI aliases.
-
-    Attributes:
-        force (bool): If True and file exists, will overwrite it.
-    """
-
-    name: str = "generate"
-    aliases: List[str] = ["g"]
-
-    @classmethod
-    def configure_args(cls, subparsers: _SubParsersAction) -> None:
-        """Configures the argument parser for the `generate` command.
-
-        Args:
-            subparsers (_SubParsersAction): The command `subparsers`.
-        """
-        parser = _add_parser(
-            subparsers,
-            name=cls.name,
-            aliases=cls.aliases,
-            help="generate a new xlbudget file",
-            cmd_cls=Generate,
-        )
-
-        parser.add_argument(
-            "-f", "--force", action="store_true", help="overwrite file if it exists"
-        )
-
-    def __init__(self, args: Namespace) -> None:
-        super().__init__(args)
-
-        if not args.force and os.path.exists(self.path):
-            raise FileExistsError(
-                f"File {self.path} exists, run with -f/--force to overwrite"
-            )
-
-        logger.debug(f"instance variables: {vars(self)}")
-
-    def run(self) -> None:
-        """Creates an empty xlbudget file populated with:
-
-        - A sheet for the current year.
-
-        Raises:
-            FileExistsError: If `self.force` is false and the file exists.
-        """
-        # create workbook without any sheets
-        wb = Workbook()
-        ws = wb.active
-        # ignore type mismatch of active worksheet
-        wb.remove(ws)  # type: ignore[arg-type]
-
-        year = datetime.date.today().year
-        logger.info(f"Creating {year} sheet")
-        create_year_sheet(wb, year)
-
-        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

- -
- - - -
- configure_args - - - - classmethod - - -
-
configure_args(subparsers)
-
- -
- -

Configures the argument parser for the generate 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 `generate` command.
-
-    Args:
-        subparsers (_SubParsersAction): The command `subparsers`.
-    """
-    parser = _add_parser(
-        subparsers,
-        name=cls.name,
-        aliases=cls.aliases,
-        help="generate a new xlbudget file",
-        cmd_cls=Generate,
-    )
-
-    parser.add_argument(
-        "-f", "--force", action="store_true", help="overwrite file if it exists"
-    )
-
-
-
- -
- -
- - - -
- run - - -
-
run()
-
- -
- -

Creates an empty xlbudget file populated with:

-
    -
  • A sheet for the current year.
  • -
- -

Raises:

- - - - - - - - - - - - - -
TypeDescription
- FileExistsError -

If self.force is false and the file exists.

- -
- Source code in src/xlbudget/commands.py -
def run(self) -> None:
-    """Creates an empty xlbudget file populated with:
-
-    - A sheet for the current year.
-
-    Raises:
-        FileExistsError: If `self.force` is false and the file exists.
-    """
-    # create workbook without any sheets
-    wb = Workbook()
-    ws = wb.active
-    # ignore type mismatch of active worksheet
-    wb.remove(ws)  # type: ignore[arg-type]
-
-    year = datetime.date.today().year
-    logger.info(f"Creating {year} sheet")
-    create_year_sheet(wb, year)
-
-    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}")
-
-
-
- -
- - - -
- -
- -
- -
- - - -

- 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 - str -

The path to the input file.

format - inputformat.InputFormat -

The input file format.

- - -
- 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 (str): The path to the input file.
-        format (inputformat.InputFormat): The input file format.
-    """
-
-    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,
-        )
-
-        parser.add_argument("input", help="path to the input file")
-        parser.add_argument(
-            "format",
-            action=GetInputFormats,
-            choices=GetInputFormats.input_formats.keys(),
-            help="select an input file format",
-        )
-
-    def __init__(self, args: Namespace) -> None:
-        super().__init__(args)
-
-        self._check_input(args.input)
-        self.input = args.input
-        self.format = args.format
-
-        logger.debug(f"instance variables: {vars(self)}")
-
-    @staticmethod
-    def _check_input(input: str) -> None:
-        """Check that `input` is a valid path to an input file.
-
-        Args:
-            input (str): The input path.
-
-        Raises:
-            ValueError: If `input` is not a CSV file.
-            ValueError: If `input` is not an existing file.
-        """
-        csv_ext = ".csv"
-        if not input.endswith(csv_ext):
-            raise ValueError(f"Input '{input}' does not end with '{csv_ext}'")
-
-        if not os.path.isfile(input):
-            raise ValueError(f"Input '{input}' is not an existing file")
-
-    def run(self) -> None:
-        logger.info(f"Parsing input file {self.input}")
-        df = parse_input(self.input, self.format)
-        logger.debug(f"input file: {df.shape=}, df.dtypes=\n{df.dtypes}")
-        logger.debug(f"df.head()=\n{df.head()}")
-
-        logger.info(f"Loading xlbudget file {self.path}")
-        wb = load_workbook(self.path)
-
-        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)
-
- -
- -

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

- -

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
input - str -

The input path.

- required -
- -

Raises:

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

If input is not a CSV file.

- ValueError -

If input is not an existing file.

- -
- Source code in src/xlbudget/commands.py -
@staticmethod
-def _check_input(input: str) -> None:
-    """Check that `input` is a valid path to an input file.
-
-    Args:
-        input (str): The input path.
-
-    Raises:
-        ValueError: If `input` is not a CSV file.
-        ValueError: If `input` is not an existing file.
-    """
-    csv_ext = ".csv"
-    if not input.endswith(csv_ext):
-        raise ValueError(f"Input '{input}' does not end with '{csv_ext}'")
-
-    if not os.path.isfile(input):
-        raise ValueError(f"Input '{input}' is not an existing file")
-
-
-
- -
- -
- - - -
- 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,
-    )
-
-    parser.add_argument("input", help="path to the input file")
-    parser.add_argument(
-        "format",
-        action=GetInputFormats,
-        choices=GetInputFormats.input_formats.keys(),
-        help="select an input file format",
-    )
-
-
-
- -
- - - -
- -
- -
-

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-13/reference/xlbudget/configure/index.html b/pr-13/reference/xlbudget/configure/index.html deleted file mode 100644 index 851f621..0000000 --- a/pr-13/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-13/reference/xlbudget/index.html b/pr-13/reference/xlbudget/index.html deleted file mode 100644 index b1a0ac9..0000000 --- a/pr-13/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-13/reference/xlbudget/inputformat/index.html b/pr-13/reference/xlbudget/inputformat/index.html deleted file mode 100644 index c69a73c..0000000 --- a/pr-13/reference/xlbudget/inputformat/index.html +++ /dev/null @@ -1,1465 +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 -
63
-64
-65
-66
-67
-68
-69
-70
-71
-72
-73
-74
-75
-76
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 indices of columns that map to COLUMNS.

ignores - List[str] -

Ignore transactions that start with these strings.

- - -
- Source code in src/xlbudget/inputformat.py -
11
-12
-13
-14
-15
-16
-17
-18
-19
-20
-21
-22
-23
-24
-25
-26
-27
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 indices of columns that map to `COLUMNS`.
-        ignores (List[str]): Ignore transactions that start with these strings.
-    """
-
-    header: int
-    names: List[str]
-    usecols: List[int]
-    ignores: List[str]
-
-    def get_usecols_names(self):
-        return [self.names[i] for i in self.usecols]
-
-
- - - -
- - - - - - - - - - - -
- -
- -
-

Functions

- -
- - - -

- parse_input - - -

-
parse_input(path, format)
-
- -
- -

Parses an input file.

- -

Parameters:

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

The path to the input file.

- required -
format - InputFormat -

The input file format.

- required -
- -

Raises:

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

If input file 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(path: str, format: InputFormat) -> pd.DataFrame:
-    """Parses an input file.
-
-    Args:
-        path (str): The path to the input file.
-        format (InputFormat): The input file format.
-
-    Raises:
-        ValueError: If input file contains duplicate transactions.
-
-    Returns:
-        A[n] `pd.DataFrame` where the columns match the xlbudget file's column names.
-    """
-    df = pd.read_csv(
-        path,
-        header=format.header,
-        usecols=format.usecols,
-        parse_dates=[0],
-        skip_blank_lines=False,
-    )
-
-    df = df_drop_na(df)
-
-    # TODO: write issues to make ignoring duplicate transactions interactive
-    # they might not be an error
-    # TODO: investigate autocompletions
-    if df.duplicated().any():
-        raise ValueError("Input file contains duplicate transactions")
-
-    df.columns = df.columns.str.strip()
-
-    # order to match `COLUMNS`
-    df = df[format.get_usecols_names()]
-
-    # rename to match `COLUMNS`
-    df = df.set_axis([c.name for c in COLUMNS], axis="columns")
-
-    # drop ignored transactions
-    for ignore in format.ignores:
-        df = df_drop_ignores(df, ignore)
-
-    return df
-
-
-
- -
- - - -
- -
- -
- - - - - - -
-
- - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pr-13/reference/xlbudget/rwxlb/index.html b/pr-13/reference/xlbudget/rwxlb/index.html deleted file mode 100644 index 920d006..0000000 --- a/pr-13/reference/xlbudget/rwxlb/index.html +++ /dev/null @@ -1,1989 +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 -
34
-35
-36
-37
-38
-39
-40
-41
-42
-43
-44
-45
-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
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 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})"
-        )
-
-    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(1, (len(COLUMNS) + 1) * num_tables + 1, len(COLUMNS) + 1):
-        month_ind = c_start // (len(COLUMNS) + 1)
-        month = MONTH_NAME_0_IND[month_ind]
-        table_name = _get_table_name(month, year_str)
-        logger.debug(f"creating {table_name} table")
-
-        # table title
-        ws.cell(row=1, column=c_start).value = month
-        ws.merge_cells(
-            start_row=1,
-            start_column=c_start,
-            end_row=1,
-            end_column=c_start + len(COLUMNS) - 2,
-        )
-
-        # table sum
-        sum = ws.cell(row=1, column=c_start + len(COLUMNS) - 1)
-        sum.value = f"=SUM({table_name}[{COLUMNS[-1].name}])"
-        sum.number_format = FORMAT_ACCOUNTING
-        logger.debug(f"created sum cell {sum.coordinate}='{sum.value}'")
-
-        # table header and formating
-        for i in range(len(COLUMNS)):
-            c = c_start + i
-
-            # header
-            ws.cell(row=2, column=c).value = COLUMNS[i].name
-
-            # column format
-            cell = ws.cell(row=3, column=c)
-            if COLUMNS[i].format:
-                cell.number_format = COLUMNS[i].format
-
-            # column width
-            ws.column_dimensions[get_column_letter(c)].width = COLUMNS[i].width
-
-        # create table
-        c_start_ltr = get_column_letter(c_start)
-        c_end_ltr = get_column_letter(c_start + len(COLUMNS) - 1)
-        ref = f"{c_start_ltr}2:{c_end_ltr}3"
-        logger.debug(f"creating table {table_name} with {ref=}")
-        tab = Table(displayName=table_name, ref=ref)
-
-        # add a default style with striped rows and banded columns
-        style = TableStyleInfo(
-            name="TableStyleMedium9",
-            showFirstColumn=False,
-            showLastColumn=False,
-            showRowStripes=True,
-            showColumnStripes=True,
-        )
-        tab.tableStyleInfo = style
-
-        ws.add_table(tab)
-
-
-
- -
- -
- - - -

- 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 that start with ignore, dropping them in place if any.

- -

Parameters:

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

The original dataframe.

- required -
ignore - str -

The string that begins descriptions to ignore.

- required -
- -

Returns:

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

A[n] pd.DataFrame without any rows that start with ignore.

- -
- Source code in src/xlbudget/rwxlb.py -
def df_drop_ignores(df: pd.DataFrame, ignore: str) -> pd.DataFrame:
-    """Checks for rows that start with `ignore`, dropping them in place if any.
-
-    Args:
-        df (pd.DataFrame): The original dataframe.
-        ignore (str): The string that begins descriptions to ignore.
-
-    Returns:
-        A[n] `pd.DataFrame` without any rows that start with `ignore`.
-    """
-    ignored = df["Description"].str.startswith(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.warning(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_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(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_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 = FORMAT_DATE
-
-        # 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 = FORMAT_ACCOUNTING
-
-        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-13/search/search_index.json b/pr-13/search/search_index.json deleted file mode 100644 index 7bdfa58..0000000 --- a/pr-13/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@v4\"\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@v3\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@v4\"\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@v4\nwith:\npython-version: \"3.10\"\ncache: pip\ncache-dependency-path: '**/requirements/docs.txt'\n- name: Download HTML coverage report\nuses: actions/download-artifact@v3\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@v4\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@v4\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 generated/modified.

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 generated/modified.\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.Generate","title":"Generate","text":"

Bases: Command

The generate command generates a new 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 force bool

If True and file exists, will overwrite it.

Source code in src/xlbudget/commands.py
class Generate(Command):\n\"\"\"The `generate` command generates a new 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        force (bool): If True and file exists, will overwrite it.\n    \"\"\"\nname: str = \"generate\"\naliases: List[str] = [\"g\"]\n@classmethod\ndef configure_args(cls, subparsers: _SubParsersAction) -> None:\n\"\"\"Configures the argument parser for the `generate` command.\n        Args:\n            subparsers (_SubParsersAction): The command `subparsers`.\n        \"\"\"\nparser = _add_parser(\nsubparsers,\nname=cls.name,\naliases=cls.aliases,\nhelp=\"generate a new xlbudget file\",\ncmd_cls=Generate,\n)\nparser.add_argument(\n\"-f\", \"--force\", action=\"store_true\", help=\"overwrite file if it exists\"\n)\ndef __init__(self, args: Namespace) -> None:\nsuper().__init__(args)\nif not args.force and os.path.exists(self.path):\nraise FileExistsError(\nf\"File {self.path} exists, run with -f/--force to overwrite\"\n)\nlogger.debug(f\"instance variables: {vars(self)}\")\ndef run(self) -> None:\n\"\"\"Creates an empty xlbudget file populated with:\n        - A sheet for the current year.\n        Raises:\n            FileExistsError: If `self.force` is false and the file exists.\n        \"\"\"\n# create workbook without any sheets\nwb = Workbook()\nws = wb.active\n# ignore type mismatch of active worksheet\nwb.remove(ws)  # type: ignore[arg-type]\nyear = datetime.date.today().year\nlogger.info(f\"Creating {year} sheet\")\ncreate_year_sheet(wb, year)\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.Generate-functions","title":"Functions","text":""},{"location":"reference/xlbudget/commands/#xlbudget.commands.Generate.configure_args","title":"configure_args classmethod","text":"
configure_args(subparsers)\n

Configures the argument parser for the generate 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 `generate` command.\n    Args:\n        subparsers (_SubParsersAction): The command `subparsers`.\n    \"\"\"\nparser = _add_parser(\nsubparsers,\nname=cls.name,\naliases=cls.aliases,\nhelp=\"generate a new xlbudget file\",\ncmd_cls=Generate,\n)\nparser.add_argument(\n\"-f\", \"--force\", action=\"store_true\", help=\"overwrite file if it exists\"\n)\n
"},{"location":"reference/xlbudget/commands/#xlbudget.commands.Generate.run","title":"run","text":"
run()\n

Creates an empty xlbudget file populated with:

  • A sheet for the current year.

Raises:

Type Description FileExistsError

If self.force is false and the file exists.

Source code in src/xlbudget/commands.py
def run(self) -> None:\n\"\"\"Creates an empty xlbudget file populated with:\n    - A sheet for the current year.\n    Raises:\n        FileExistsError: If `self.force` is false and the file exists.\n    \"\"\"\n# create workbook without any sheets\nwb = Workbook()\nws = wb.active\n# ignore type mismatch of active worksheet\nwb.remove(ws)  # type: ignore[arg-type]\nyear = datetime.date.today().year\nlogger.info(f\"Creating {year} sheet\")\ncreate_year_sheet(wb, year)\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","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 str

The path to the input file.

format inputformat.InputFormat

The input file format.

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 (str): The path to the input file.\n        format (inputformat.InputFormat): The input file format.\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)\nparser.add_argument(\"input\", help=\"path to the input file\")\nparser.add_argument(\n\"format\",\naction=GetInputFormats,\nchoices=GetInputFormats.input_formats.keys(),\nhelp=\"select an input file format\",\n)\ndef __init__(self, args: Namespace) -> None:\nsuper().__init__(args)\nself._check_input(args.input)\nself.input = args.input\nself.format = args.format\nlogger.debug(f\"instance variables: {vars(self)}\")\n@staticmethod\ndef _check_input(input: str) -> None:\n\"\"\"Check that `input` is a valid path to an input file.\n        Args:\n            input (str): The input path.\n        Raises:\n            ValueError: If `input` is not a CSV file.\n            ValueError: If `input` is not an existing file.\n        \"\"\"\ncsv_ext = \".csv\"\nif not input.endswith(csv_ext):\nraise ValueError(f\"Input '{input}' does not end with '{csv_ext}'\")\nif not os.path.isfile(input):\nraise ValueError(f\"Input '{input}' is not an existing file\")\ndef run(self) -> None:\nlogger.info(f\"Parsing input file {self.input}\")\ndf = parse_input(self.input, self.format)\nlogger.debug(f\"input file: {df.shape=}, df.dtypes=\\n{df.dtypes}\")\nlogger.debug(f\"df.head()=\\n{df.head()}\")\nlogger.info(f\"Loading xlbudget file {self.path}\")\nwb = load_workbook(self.path)\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)\n

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

Parameters:

Name Type Description Default input str

The input path.

required

Raises:

Type Description ValueError

If input is not a CSV file.

ValueError

If input is not an existing file.

Source code in src/xlbudget/commands.py
@staticmethod\ndef _check_input(input: str) -> None:\n\"\"\"Check that `input` is a valid path to an input file.\n    Args:\n        input (str): The input path.\n    Raises:\n        ValueError: If `input` is not a CSV file.\n        ValueError: If `input` is not an existing file.\n    \"\"\"\ncsv_ext = \".csv\"\nif not input.endswith(csv_ext):\nraise ValueError(f\"Input '{input}' does not end with '{csv_ext}'\")\nif not os.path.isfile(input):\nraise ValueError(f\"Input '{input}' is not an existing file\")\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)\nparser.add_argument(\"input\", help=\"path to the input file\")\nparser.add_argument(\n\"format\",\naction=GetInputFormats,\nchoices=GetInputFormats.input_formats.keys(),\nhelp=\"select an input file format\",\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 indices of columns that map to COLUMNS.

ignores List[str]

Ignore transactions that start with these strings.

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 indices of columns that map to `COLUMNS`.\n        ignores (List[str]): Ignore transactions that start with these strings.\n    \"\"\"\nheader: int\nnames: List[str]\nusecols: List[int]\nignores: List[str]\ndef get_usecols_names(self):\nreturn [self.names[i] for i in self.usecols]\n
"},{"location":"reference/xlbudget/inputformat/#xlbudget.inputformat-functions","title":"Functions","text":""},{"location":"reference/xlbudget/inputformat/#xlbudget.inputformat.parse_input","title":"parse_input","text":"
parse_input(path, format)\n

Parses an input file.

Parameters:

Name Type Description Default path str

The path to the input file.

required format InputFormat

The input file format.

required

Raises:

Type Description ValueError

If input file 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(path: str, format: InputFormat) -> pd.DataFrame:\n\"\"\"Parses an input file.\n    Args:\n        path (str): The path to the input file.\n        format (InputFormat): The input file format.\n    Raises:\n        ValueError: If input file contains duplicate transactions.\n    Returns:\n        A[n] `pd.DataFrame` where the columns match the xlbudget file's column names.\n    \"\"\"\ndf = pd.read_csv(\npath,\nheader=format.header,\nusecols=format.usecols,\nparse_dates=[0],\nskip_blank_lines=False,\n)\ndf = df_drop_na(df)\n# TODO: write issues to make ignoring duplicate transactions interactive\n# they might not be an error\n# TODO: investigate autocompletions\nif df.duplicated().any():\nraise ValueError(\"Input file contains duplicate transactions\")\ndf.columns = df.columns.str.strip()\n# order to match `COLUMNS`\ndf = df[format.get_usecols_names()]\n# rename to match `COLUMNS`\ndf = df.set_axis([c.name for c in COLUMNS], axis=\"columns\")\n# drop ignored transactions\nfor ignore in format.ignores:\ndf = df_drop_ignores(df, ignore)\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 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})\"\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(1, (len(COLUMNS) + 1) * num_tables + 1, len(COLUMNS) + 1):\nmonth_ind = c_start // (len(COLUMNS) + 1)\nmonth = MONTH_NAME_0_IND[month_ind]\ntable_name = _get_table_name(month, year_str)\nlogger.debug(f\"creating {table_name} table\")\n# table title\nws.cell(row=1, column=c_start).value = month\nws.merge_cells(\nstart_row=1,\nstart_column=c_start,\nend_row=1,\nend_column=c_start + len(COLUMNS) - 2,\n)\n# table sum\nsum = ws.cell(row=1, column=c_start + len(COLUMNS) - 1)\nsum.value = f\"=SUM({table_name}[{COLUMNS[-1].name}])\"\nsum.number_format = FORMAT_ACCOUNTING\nlogger.debug(f\"created sum cell {sum.coordinate}='{sum.value}'\")\n# table header and formating\nfor i in range(len(COLUMNS)):\nc = c_start + i\n# header\nws.cell(row=2, column=c).value = COLUMNS[i].name\n# column format\ncell = ws.cell(row=3, column=c)\nif COLUMNS[i].format:\ncell.number_format = COLUMNS[i].format\n# column width\nws.column_dimensions[get_column_letter(c)].width = COLUMNS[i].width\n# create table\nc_start_ltr = get_column_letter(c_start)\nc_end_ltr = get_column_letter(c_start + len(COLUMNS) - 1)\nref = f\"{c_start_ltr}2:{c_end_ltr}3\"\nlogger.debug(f\"creating table {table_name} with {ref=}\")\ntab = Table(displayName=table_name, ref=ref)\n# add a default style with striped rows and banded columns\nstyle = TableStyleInfo(\nname=\"TableStyleMedium9\",\nshowFirstColumn=False,\nshowLastColumn=False,\nshowRowStripes=True,\nshowColumnStripes=True,\n)\ntab.tableStyleInfo = style\nws.add_table(tab)\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 that start with ignore, dropping them in place if any.

Parameters:

Name Type Description Default df pd.DataFrame

The original dataframe.

required ignore str

The string that begins descriptions to ignore.

required

Returns:

Type Description pd.DataFrame

A[n] pd.DataFrame without any rows that start with ignore.

Source code in src/xlbudget/rwxlb.py
def df_drop_ignores(df: pd.DataFrame, ignore: str) -> pd.DataFrame:\n\"\"\"Checks for rows that start with `ignore`, dropping them in place if any.\n    Args:\n        df (pd.DataFrame): The original dataframe.\n        ignore (str): The string that begins descriptions to ignore.\n    Returns:\n        A[n] `pd.DataFrame` without any rows that start with `ignore`.\n    \"\"\"\nignored = df[\"Description\"].str.startswith(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.warning(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_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(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_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 = FORMAT_DATE\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 = FORMAT_ACCOUNTING\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-13/sitemap.xml b/pr-13/sitemap.xml deleted file mode 100644 index bdc36f4..0000000 --- a/pr-13/sitemap.xml +++ /dev/null @@ -1,88 +0,0 @@ - - - - https://patrick-5546.github.io/xlbudget/pr-13/ - 2023-12-23 - daily - - - https://patrick-5546.github.io/xlbudget/pr-13/coverage/ - 2023-12-23 - daily - - - https://patrick-5546.github.io/xlbudget/pr-13/license/ - 2023-12-23 - daily - - - https://patrick-5546.github.io/xlbudget/pr-13/developer_guide/contributing/ - 2023-12-23 - daily - - - https://patrick-5546.github.io/xlbudget/pr-13/developer_guide/docs/ - 2023-12-23 - daily - - - https://patrick-5546.github.io/xlbudget/pr-13/developer_guide/github_actions/ - 2023-12-23 - daily - - - https://patrick-5546.github.io/xlbudget/pr-13/developer_guide/releases/ - 2023-12-23 - daily - - - https://patrick-5546.github.io/xlbudget/pr-13/developer_guide/vscode/ - 2023-12-23 - daily - - - https://patrick-5546.github.io/xlbudget/pr-13/reference/xlbudget/ - 2023-12-23 - daily - - - https://patrick-5546.github.io/xlbudget/pr-13/reference/xlbudget/commands/ - 2023-12-23 - daily - - - https://patrick-5546.github.io/xlbudget/pr-13/reference/xlbudget/configure/ - 2023-12-23 - daily - - - https://patrick-5546.github.io/xlbudget/pr-13/reference/xlbudget/inputformat/ - 2023-12-23 - daily - - - https://patrick-5546.github.io/xlbudget/pr-13/reference/xlbudget/rwxlb/ - 2023-12-23 - daily - - - https://patrick-5546.github.io/xlbudget/pr-13/user_guide/commands/ - 2023-12-23 - daily - - - https://patrick-5546.github.io/xlbudget/pr-13/user_guide/configuration/ - 2023-12-23 - daily - - - https://patrick-5546.github.io/xlbudget/pr-13/user_guide/getting_started/ - 2023-12-23 - daily - - - https://patrick-5546.github.io/xlbudget/pr-13/user_guide/installation/ - 2023-12-23 - daily - - \ No newline at end of file diff --git a/pr-13/sitemap.xml.gz b/pr-13/sitemap.xml.gz deleted file mode 100644 index 2b157c6f2429cc88296fdec4260bf589e21fafa6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 370 zcmV-&0ge72iwFqGOowFx|8r?{Wo=<_E_iKh0M(blZo?o9K=1vEDEEXmU8k)|)7!pa zJ69AOf<_?2&~$%4r>VB=JZTbK0As9YFc|9NwKv%XJZfhT`KDaw8Cc<@w(5|-y}lGr z`LS8m(Hn%2k+Q`@-i0vi85_s3L>-{#1D4J!hN7bmvb&J2%jdjVWmF~ubL&=W<3uxA zK{7M&;I()!cDskiQt8mebEzG>nsby20vmj>*|M5VbCxDJhTh4hT36d*Qx(;gRPM5c zn&`My(0cf2B-iG%W(#=hbHjwu{1CRdOOuh9Z5CW9;C0>%yCPbz8&wA_}aonxSQ zP1OhK1g6S9E1tLrbblo4g$Dy1AzA(l(v6+W8TQbErz8VE1L=|r*Z15?Oxgw8Y88FT z0i^8^L+gCcQw}CSQZp6)h>I+o)*<+XJ_kRa5?F%#B%IzpCk)&NkO_LAEwLN(PaANn Q=quBF0Su - - - - - - - - - - - - - - - - - - - - - - - - - - Commands - Xlbudget Docs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - - -
- - - - - - - -
- -
- - - - -
-
- - - -
-
-
- - - - - - -
-
-
- - - -
-
-
- - - -
-
-
- - - -
-
- - - - - - - - - - - - - - - - - - - - -

Commands

- -
-
- - - Last update: - March 19, 2023 - - - -
- - - - - - -
-
- - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pr-13/user_guide/configuration/index.html b/pr-13/user_guide/configuration/index.html deleted file mode 100644 index 99a9940..0000000 --- a/pr-13/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-13/user_guide/getting_started/index.html b/pr-13/user_guide/getting_started/index.html deleted file mode 100644 index b151acb..0000000 --- a/pr-13/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-13/user_guide/installation/index.html b/pr-13/user_guide/installation/index.html deleted file mode 100644 index 9ca11ca..0000000 --- a/pr-13/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 a60660d..221a221 100644 --- a/versions.json +++ b/versions.json @@ -1 +1 @@ -[{"version": "pr-13", "title": "pr-13", "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