E(null,null,function*(){let e=t.data;switch(e.type){case 0:Z=yield se(e.data.items,{separator:Zt(e.data.config.separator),transform:G(Gt),parser:r=>ae(r,"and",l=>({field:"*",value:ie(l.value),range:{start:l.start,end:l.end,value:l.index}})),compiler:le,fields:[C("title",r=>r.title,{weight:3}),C("text",r=>r.text),C("path",r=>r.path,{weight:2})],plugins:[he(),we(),pe({compiler:st,fields:[C("tags",r=>r.tags)],plugins:[oe({handlers:[ue()]})]}),qe({handlers:[r=>Ee({fields:r.fields,comparators:[Re]})],defaults:{order:[{type:"match",data:{field:"*"}}]}}),()=>q({onTextResult(r){r.total=r.items.length}}),ze({handler:Se,size:10}),Me({handler:r=>Te(r)}),()=>q({onTextResult(r){let{query:l}=r,o=l.values.map(({range:a,value:s})=>{var i,d;let u=!1;return s.forEach((f,c)=>{!u&&c<1&&(u=!0)}),u?-1:((i=a==null?void 0:a.end)!=null?i:0)-((d=a==null?void 0:a.start)!=null?d:0)});X(r,a=>{var s;(s=a.value.highlight)==null||s.ranges.forEach(u=>{u.value=o[u.value]})})}})]}),self.postMessage({type:1});break;case 2:let n=Z({type:"text",data:e.data});self.postMessage({type:3,data:n.data});break}})});var _e=qt(xt());})();
diff --git a/v2.2/assets/stylesheets/classic/main.320d0ab1.min.css b/v2.2/assets/stylesheets/classic/main.320d0ab1.min.css
new file mode 100644
index 0000000..c368965
--- /dev/null
+++ b/v2.2/assets/stylesheets/classic/main.320d0ab1.min.css
@@ -0,0 +1 @@
+@charset "UTF-8";html{-webkit-text-size-adjust:none;-moz-text-size-adjust:none;text-size-adjust:none;box-sizing:border-box}*,:after,:before{box-sizing:inherit}@media (prefers-reduced-motion){*,:after,:before{transition:none!important}}body{margin:0}a,button,input,label{-webkit-tap-highlight-color:transparent}a{color:inherit;text-decoration:none}hr{border:0;box-sizing:initial;display:block;height:.05rem;overflow:visible;padding:0}small{font-size:80%}sub,sup{line-height:1em}img{border-style:none}table{border-collapse:initial;border-spacing:0}td,th{font-weight:400;vertical-align:top}button{background:#0000;border:0;font-family:inherit;font-size:inherit;margin:0;padding:0}input{border:0;outline:none}:root{--md-primary-fg-color:#4051b5;--md-primary-fg-color--light:#5d6cc0;--md-primary-fg-color--dark:#303fa1;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3;--md-accent-fg-color:#526cfe;--md-accent-fg-color--transparent:#526cfe1a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-scheme=default]{color-scheme:light}[data-md-color-scheme=default] img[src$="#gh-dark-mode-only"],[data-md-color-scheme=default] img[src$="#only-dark"]{display:none}:root,[data-md-color-scheme=default]{--md-hue:225deg;--md-default-fg-color:#000000de;--md-default-fg-color--light:#0000008a;--md-default-fg-color--lighter:#00000052;--md-default-fg-color--lightest:#00000012;--md-default-bg-color:#fff;--md-default-bg-color--light:#ffffffb3;--md-default-bg-color--lighter:#ffffff4d;--md-default-bg-color--lightest:#ffffff1f;--md-code-fg-color:#36464e;--md-code-bg-color:#f5f5f5;--md-code-bg-color--light:#f5f5f5b3;--md-code-bg-color--lighter:#f5f5f54d;--md-code-hl-color:#4287ff;--md-code-hl-color--light:#4287ff1a;--md-code-hl-number-color:#d52a2a;--md-code-hl-special-color:#db1457;--md-code-hl-function-color:#a846b9;--md-code-hl-constant-color:#6e59d9;--md-code-hl-keyword-color:#3f6ec6;--md-code-hl-string-color:#1c7d4d;--md-code-hl-name-color:var(--md-code-fg-color);--md-code-hl-operator-color:var(--md-default-fg-color--light);--md-code-hl-punctuation-color:var(--md-default-fg-color--light);--md-code-hl-comment-color:var(--md-default-fg-color--light);--md-code-hl-generic-color:var(--md-default-fg-color--light);--md-code-hl-variable-color:var(--md-default-fg-color--light);--md-typeset-color:var(--md-default-fg-color);--md-typeset-a-color:var(--md-primary-fg-color);--md-typeset-del-color:#f5503d26;--md-typeset-ins-color:#0bd57026;--md-typeset-kbd-color:#fafafa;--md-typeset-kbd-accent-color:#fff;--md-typeset-kbd-border-color:#b8b8b8;--md-typeset-mark-color:#ffff0080;--md-typeset-table-color:#0000001f;--md-typeset-table-color--light:rgba(0,0,0,.035);--md-admonition-fg-color:var(--md-default-fg-color);--md-admonition-bg-color:var(--md-default-bg-color);--md-warning-fg-color:#000000de;--md-warning-bg-color:#ff9;--md-footer-fg-color:#fff;--md-footer-fg-color--light:#ffffffb3;--md-footer-fg-color--lighter:#ffffff73;--md-footer-bg-color:#000000de;--md-footer-bg-color--dark:#00000052;--md-shadow-z1:0 0.2rem 0.5rem #0000000d,0 0 0.05rem #0000001a;--md-shadow-z2:0 0.2rem 0.5rem #0000001a,0 0 0.05rem #00000040;--md-shadow-z3:0 0.2rem 0.5rem #0003,0 0 0.05rem #00000059;--color-foreground:0 0 0;--color-background:255 255 255;--color-background-subtle:240 240 240;--color-backdrop:255 255 255}.md-icon svg{fill:currentcolor;display:block;height:1.2rem;width:1.2rem}.md-icon svg.lucide{fill:#0000;stroke:currentcolor}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;--md-text-font-family:var(--md-text-font,_),-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;--md-code-font-family:var(--md-code-font,_),SFMono-Regular,Consolas,Menlo,monospace}aside,body,input{font-feature-settings:"kern","liga";color:var(--md-typeset-color);font-family:var(--md-text-font-family)}code,kbd,pre{font-feature-settings:"kern";font-family:var(--md-code-font-family)}:root{--md-typeset-table-sort-icon:url('data:image/svg+xml;charset=utf-8, ');--md-typeset-table-sort-icon--asc:url('data:image/svg+xml;charset=utf-8, ');--md-typeset-table-sort-icon--desc:url('data:image/svg+xml;charset=utf-8, ')}.md-typeset{-webkit-print-color-adjust:exact;color-adjust:exact;font-size:.8rem;line-height:1.6;overflow-wrap:break-word}@media print{.md-typeset{font-size:.68rem}}.md-typeset blockquote,.md-typeset dl,.md-typeset figure,.md-typeset ol,.md-typeset pre,.md-typeset ul{margin-bottom:1em;margin-top:1em}.md-typeset h1{color:var(--md-default-fg-color--light);font-size:2em;line-height:1.3;margin:0 0 1.25em}.md-typeset h1,.md-typeset h2{font-weight:300;letter-spacing:-.01em}.md-typeset h2{font-size:1.5625em;line-height:1.4;margin:1.6em 0 .64em}.md-typeset h3{font-size:1.25em;font-weight:400;letter-spacing:-.01em;line-height:1.5;margin:1.6em 0 .8em}.md-typeset h2+h3{margin-top:.8em}.md-typeset h4{font-weight:700;letter-spacing:-.01em;margin:1em 0}.md-typeset h5,.md-typeset h6{color:var(--md-default-fg-color--light);font-size:.8em;font-weight:700;letter-spacing:-.01em;margin:1.25em 0}.md-typeset h5{text-transform:uppercase}.md-typeset h5 code{text-transform:none}.md-typeset hr{border-bottom:.05rem solid var(--md-default-fg-color--lightest);display:flow-root;margin:1.5em 0}.md-typeset a{color:var(--md-typeset-a-color);word-break:break-word}.md-typeset a,.md-typeset a:before{transition:color 125ms}.md-typeset a:focus,.md-typeset a:hover{color:var(--md-accent-fg-color)}.md-typeset a:focus code,.md-typeset a:hover code{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-typeset a code{color:var(--md-typeset-a-color)}.md-typeset a.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-typeset code,.md-typeset kbd,.md-typeset pre{color:var(--md-code-fg-color);direction:ltr;font-variant-ligatures:none;transition:background-color 125ms}@media print{.md-typeset code,.md-typeset kbd,.md-typeset pre{white-space:pre-wrap}}.md-typeset code{background-color:var(--md-code-bg-color);border-radius:.1rem;-webkit-box-decoration-break:clone;box-decoration-break:clone;font-size:.85em;padding:0 .2941176471em;transition:color 125ms,background-color 125ms;word-break:break-word}.md-typeset code:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}.md-typeset pre{display:flow-root;line-height:1.4;position:relative}.md-typeset pre>code{-webkit-box-decoration-break:slice;box-decoration-break:slice;box-shadow:none;display:block;margin:0;outline-color:var(--md-accent-fg-color);overflow:auto;padding:.7720588235em 1.1764705882em;scrollbar-color:var(--md-default-fg-color--lighter) #0000;scrollbar-width:thin;touch-action:auto;word-break:normal}.md-typeset pre>code:hover{scrollbar-color:var(--md-accent-fg-color) #0000}.md-typeset pre>code::-webkit-scrollbar{height:.2rem;width:.2rem}.md-typeset pre>code::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-typeset pre>code::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}.md-typeset kbd{background-color:var(--md-typeset-kbd-color);border-radius:.1rem;box-shadow:0 .1rem 0 .05rem var(--md-typeset-kbd-border-color),0 .1rem 0 var(--md-typeset-kbd-border-color),0 -.1rem .2rem var(--md-typeset-kbd-accent-color) inset;color:var(--md-default-fg-color);display:inline-block;font-size:.75em;padding:0 .6666666667em;vertical-align:text-top;word-break:break-word}.md-typeset mark{background-color:var(--md-typeset-mark-color);-webkit-box-decoration-break:clone;box-decoration-break:clone;color:inherit;word-break:break-word}.md-typeset abbr{cursor:help;text-decoration:none}.md-typeset [data-preview],.md-typeset abbr{border-bottom:.05rem dotted var(--md-default-fg-color--light)}.md-typeset small{opacity:.75}[dir=ltr] .md-typeset sub,[dir=ltr] .md-typeset sup{margin-left:.078125em}[dir=rtl] .md-typeset sub,[dir=rtl] .md-typeset sup{margin-right:.078125em}[dir=ltr] .md-typeset blockquote{padding-left:.6rem}[dir=rtl] .md-typeset blockquote{padding-right:.6rem}[dir=ltr] .md-typeset blockquote{border-left:.2rem solid var(--md-default-fg-color--lighter)}[dir=rtl] .md-typeset blockquote{border-right:.2rem solid var(--md-default-fg-color--lighter)}.md-typeset blockquote{color:var(--md-default-fg-color--light);margin-left:0;margin-right:0}.md-typeset ul{list-style-type:disc}.md-typeset ul[type]{list-style-type:revert-layer}[dir=ltr] .md-typeset ol,[dir=ltr] .md-typeset ul{margin-left:.625em}[dir=rtl] .md-typeset ol,[dir=rtl] .md-typeset ul{margin-right:.625em}.md-typeset ol,.md-typeset ul{padding:0}.md-typeset ol:not([hidden]),.md-typeset ul:not([hidden]){display:flow-root}.md-typeset ol ol,.md-typeset ul ol{list-style-type:lower-alpha}.md-typeset ol ol ol,.md-typeset ul ol ol{list-style-type:lower-roman}.md-typeset ol ol ol ol,.md-typeset ul ol ol ol{list-style-type:upper-alpha}.md-typeset ol ol ol ol ol,.md-typeset ul ol ol ol ol{list-style-type:upper-roman}.md-typeset ol[type],.md-typeset ul[type]{list-style-type:revert-layer}[dir=ltr] .md-typeset ol li,[dir=ltr] .md-typeset ul li{margin-left:1.25em}[dir=rtl] .md-typeset ol li,[dir=rtl] .md-typeset ul li{margin-right:1.25em}.md-typeset ol li,.md-typeset ul li{margin-bottom:.5em}.md-typeset ol li blockquote,.md-typeset ol li p,.md-typeset ul li blockquote,.md-typeset ul li p{margin:.5em 0}.md-typeset ol li:last-child,.md-typeset ul li:last-child{margin-bottom:0}[dir=ltr] .md-typeset ol li ol,[dir=ltr] .md-typeset ol li ul,[dir=ltr] .md-typeset ul li ol,[dir=ltr] .md-typeset ul li ul{margin-left:.625em}[dir=rtl] .md-typeset ol li ol,[dir=rtl] .md-typeset ol li ul,[dir=rtl] .md-typeset ul li ol,[dir=rtl] .md-typeset ul li ul{margin-right:.625em}.md-typeset ol li ol,.md-typeset ol li ul,.md-typeset ul li ol,.md-typeset ul li ul{margin-bottom:.5em;margin-top:.5em}[dir=ltr] .md-typeset dd{margin-left:1.875em}[dir=rtl] .md-typeset dd{margin-right:1.875em}.md-typeset dd{margin-bottom:1.5em;margin-top:1em}.md-typeset img,.md-typeset svg,.md-typeset video{height:auto;max-width:100%}.md-typeset img[align=left]{margin:1em 1em 1em 0}.md-typeset img[align=right]{margin:1em 0 1em 1em}.md-typeset img[align]:only-child{margin-top:0}.md-typeset figure{display:flow-root;margin:1em auto;max-width:100%;text-align:center;width:fit-content}.md-typeset figure img{display:block;margin:0 auto}.md-typeset figcaption{font-style:italic;margin:1em auto;max-width:24rem}.md-typeset iframe{max-width:100%}.md-typeset table:not([class]){background-color:var(--md-default-bg-color);border:.05rem solid var(--md-typeset-table-color);border-radius:.1rem;display:inline-block;font-size:.64rem;max-width:100%;overflow:auto;touch-action:auto}@media print{.md-typeset table:not([class]){display:table}}.md-typeset table:not([class])+*{margin-top:1.5em}.md-typeset table:not([class]) td>:first-child,.md-typeset table:not([class]) th>:first-child{margin-top:0}.md-typeset table:not([class]) td>:last-child,.md-typeset table:not([class]) th>:last-child{margin-bottom:0}.md-typeset table:not([class]) td:not([align]),.md-typeset table:not([class]) th:not([align]){text-align:left}[dir=rtl] .md-typeset table:not([class]) td:not([align]),[dir=rtl] .md-typeset table:not([class]) th:not([align]){text-align:right}.md-typeset table:not([class]) th{font-weight:700;min-width:5rem;padding:.9375em 1.25em;vertical-align:top}.md-typeset table:not([class]) td{border-top:.05rem solid var(--md-typeset-table-color);padding:.9375em 1.25em;vertical-align:top}.md-typeset table:not([class]) tbody tr{transition:background-color 125ms}.md-typeset table:not([class]) tbody tr:hover{background-color:var(--md-typeset-table-color--light);box-shadow:0 .05rem 0 var(--md-default-bg-color) inset}.md-typeset table:not([class]) a{word-break:normal}.md-typeset table th[role=columnheader]{cursor:pointer}[dir=ltr] .md-typeset table th[role=columnheader]:after{margin-left:.5em}[dir=rtl] .md-typeset table th[role=columnheader]:after{margin-right:.5em}.md-typeset table th[role=columnheader]:after{content:"";display:inline-block;height:1.2em;-webkit-mask-image:var(--md-typeset-table-sort-icon);mask-image:var(--md-typeset-table-sort-icon);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:background-color 125ms;vertical-align:text-bottom;width:1.2em}.md-typeset table th[role=columnheader]:hover:after{background-color:var(--md-default-fg-color--lighter)}.md-typeset table th[role=columnheader][aria-sort=ascending]:after{background-color:var(--md-default-fg-color--light);-webkit-mask-image:var(--md-typeset-table-sort-icon--asc);mask-image:var(--md-typeset-table-sort-icon--asc)}.md-typeset table th[role=columnheader][aria-sort=descending]:after{background-color:var(--md-default-fg-color--light);-webkit-mask-image:var(--md-typeset-table-sort-icon--desc);mask-image:var(--md-typeset-table-sort-icon--desc)}.md-typeset__scrollwrap{margin:1em -.8rem;overflow-x:auto;touch-action:auto}.md-typeset__table{display:inline-block;margin-bottom:.5em;padding:0 .8rem}@media print{.md-typeset__table{display:block}}html .md-typeset__table table{display:table;margin:0;overflow:hidden;width:100%}@media screen and (max-width:44.984375em){.md-content__inner>pre{margin:1em -.8rem}.md-content__inner>pre code{border-radius:0}}.md-typeset .md-author{border-radius:100%;display:block;flex-shrink:0;height:1.6rem;overflow:hidden;position:relative;transition:color 125ms,transform 125ms;width:1.6rem}.md-typeset .md-author img{display:block}.md-typeset .md-author--more{background:var(--md-default-fg-color--lightest);color:var(--md-default-fg-color--lighter);font-size:.6rem;font-weight:700;line-height:1.6rem;text-align:center}.md-typeset .md-author--long{height:2.4rem;width:2.4rem}.md-typeset a.md-author{transform:scale(1)}.md-typeset a.md-author img{border-radius:100%;filter:grayscale(100%) opacity(75%);transition:filter 125ms}.md-typeset a.md-author:focus,.md-typeset a.md-author:hover{transform:scale(1.1);z-index:1}.md-typeset a.md-author:focus img,.md-typeset a.md-author:hover img{filter:grayscale(0)}.md-banner{background-color:var(--md-footer-bg-color);color:var(--md-footer-fg-color);overflow:auto}@media print{.md-banner{display:none}}.md-banner--warning{background-color:var(--md-warning-bg-color);color:var(--md-warning-fg-color)}.md-banner__inner{font-size:.7rem;margin:.6rem auto;padding:0 .8rem}[dir=ltr] .md-banner__button{float:right}[dir=rtl] .md-banner__button{float:left}.md-banner__button{color:inherit;cursor:pointer;transition:opacity .25s}.no-js .md-banner__button{display:none}.md-banner__button:hover{opacity:.7}html{scrollbar-gutter:stable;font-size:125%;height:100%;overflow-x:hidden}@media screen and (min-width:100em){html{font-size:137.5%}}@media screen and (min-width:125em){html{font-size:150%}}body{background-color:var(--md-default-bg-color);display:flex;flex-direction:column;font-size:.5rem;min-height:100%;position:relative;width:100%}@media print{body{display:block}}@media screen and (max-width:59.984375em){body[data-md-scrolllock]{position:fixed}}.md-grid{margin-left:auto;margin-right:auto;max-width:61rem}.md-container{display:flex;flex-direction:column;flex-grow:1}@media print{.md-container{display:block}}.md-main{flex-grow:1}.md-main__inner{display:flex;height:100%;margin-top:1.5rem}.md-ellipsis{overflow:hidden;text-overflow:ellipsis}.md-toggle{display:none}.md-option{height:0;opacity:0;position:absolute;width:0}.md-option:checked+label:not([hidden]){display:block}.md-option.focus-visible+label{outline-color:var(--md-accent-fg-color);outline-style:auto}.md-skip{background-color:var(--md-default-fg-color);border-radius:.1rem;color:var(--md-default-bg-color);font-size:.64rem;margin:.5rem;opacity:0;outline-color:var(--md-accent-fg-color);padding:.3rem .5rem;position:fixed;transform:translateY(.4rem);z-index:-1}.md-skip:focus{opacity:1;transform:translateY(0);transition:transform .25s cubic-bezier(.4,0,.2,1),opacity 175ms 75ms;z-index:10}@page{margin:25mm}:root{--md-clipboard-icon:url('data:image/svg+xml;charset=utf-8, ')}.md-clipboard{border-radius:.1rem;color:var(--md-default-fg-color--lightest);cursor:pointer;height:1.5em;outline-color:var(--md-accent-fg-color);outline-offset:.1rem;transition:color .25s;width:1.5em;z-index:1}@media print{.md-clipboard{display:none}}.md-clipboard:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}:hover>.md-clipboard{color:var(--md-default-fg-color--light)}.md-clipboard:focus,.md-clipboard:hover{color:var(--md-accent-fg-color)}.md-clipboard:after{background-color:currentcolor;content:"";display:block;height:1.125em;margin:0 auto;-webkit-mask-image:var(--md-clipboard-icon);mask-image:var(--md-clipboard-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:1.125em}.md-clipboard--inline{cursor:pointer}.md-clipboard--inline code{transition:color .25s,background-color .25s}.md-clipboard--inline:focus code,.md-clipboard--inline:hover code{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}:root{--md-code-select-icon:url('data:image/svg+xml;charset=utf-8, ');--md-code-copy-icon:url('data:image/svg+xml;charset=utf-8, ')}.md-typeset .md-code__content{display:grid}.md-code__nav{background-color:var(--md-code-bg-color--lighter);border-radius:.1rem;display:flex;gap:.2rem;padding:.2rem;position:absolute;right:.25em;top:.25em;transition:background-color .25s;z-index:1}:hover>.md-code__nav{background-color:var(--md-code-bg-color--light)}.md-code__button{color:var(--md-default-fg-color--lightest);cursor:pointer;display:block;height:1.5em;outline-color:var(--md-accent-fg-color);outline-offset:.1rem;transition:color .25s;width:1.5em}:hover>*>.md-code__button{color:var(--md-default-fg-color--light)}.md-code__button.focus-visible,.md-code__button:hover{color:var(--md-accent-fg-color)}.md-code__button--active{color:var(--md-default-fg-color)!important}.md-code__button:after{background-color:currentcolor;content:"";display:block;height:1.125em;margin:0 auto;-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:1.125em}.md-code__button[data-md-type=select]:after{-webkit-mask-image:var(--md-code-select-icon);mask-image:var(--md-code-select-icon)}.md-code__button[data-md-type=copy]:after{-webkit-mask-image:var(--md-code-copy-icon);mask-image:var(--md-code-copy-icon)}@keyframes consent{0%{opacity:0;transform:translateY(100%)}to{opacity:1;transform:translateY(0)}}@keyframes overlay{0%{opacity:0}to{opacity:1}}.md-consent__overlay{animation:overlay .25s both;-webkit-backdrop-filter:blur(.1rem);backdrop-filter:blur(.1rem);background-color:#0000008a;height:100%;opacity:1;position:fixed;top:0;width:100%;z-index:5}.md-consent__inner{animation:consent .5s cubic-bezier(.1,.7,.1,1) both;background-color:var(--md-default-bg-color);border:0;border-radius:.1rem;bottom:0;box-shadow:0 0 .2rem #0000001a,0 .2rem .4rem #0003;max-height:100%;overflow:auto;padding:0;position:fixed;width:100%;z-index:5}.md-consent__form{padding:.8rem}.md-consent__settings{display:none;margin:1em 0}input:checked+.md-consent__settings{display:block}.md-consent__controls{margin-bottom:.8rem}.md-typeset .md-consent__controls .md-button{display:inline}@media screen and (max-width:44.984375em){.md-typeset .md-consent__controls .md-button{display:block;margin-top:.4rem;text-align:center;width:100%}}.md-consent label{cursor:pointer}.md-content{flex-grow:1;min-width:0}.md-content__inner{margin:0 .8rem 1.2rem;padding-top:.6rem}@media screen and (min-width:76.25em){[dir=ltr] .md-sidebar--primary:not([hidden])~.md-content>.md-content__inner{margin-left:1.2rem}[dir=ltr] .md-sidebar--secondary:not([hidden])~.md-content>.md-content__inner,[dir=rtl] .md-sidebar--primary:not([hidden])~.md-content>.md-content__inner{margin-right:1.2rem}[dir=rtl] .md-sidebar--secondary:not([hidden])~.md-content>.md-content__inner{margin-left:1.2rem}}.md-content__inner:before{content:"";display:block;height:.4rem}.md-content__inner>:last-child{margin-bottom:0}[dir=ltr] .md-content__button{float:right}[dir=rtl] .md-content__button{float:left}[dir=ltr] .md-content__button{margin-left:.4rem}[dir=rtl] .md-content__button{margin-right:.4rem}.md-content__button{margin:.4rem 0;padding:0}@media print{.md-content__button{display:none}}.md-typeset .md-content__button{color:var(--md-default-fg-color--lighter)}.md-content__button svg{display:inline;vertical-align:top}[dir=rtl] .md-content__button svg{transform:scaleX(-1)}.md-content__button svg.lucide{fill:#0000;stroke:currentcolor}[dir=ltr] .md-dialog{right:.8rem}[dir=rtl] .md-dialog{left:.8rem}.md-dialog{background-color:var(--md-default-fg-color);border-radius:.1rem;bottom:.8rem;box-shadow:var(--md-shadow-z3);min-width:11.1rem;opacity:0;padding:.4rem .6rem;pointer-events:none;position:fixed;transform:translateY(100%);transition:transform 0ms .4s,opacity .4s;z-index:4}@media print{.md-dialog{display:none}}.md-dialog--active{opacity:1;pointer-events:auto;transform:translateY(0);transition:transform .4s cubic-bezier(.075,.85,.175,1),opacity .4s}.md-dialog__inner{color:var(--md-default-bg-color);font-size:.7rem}.md-feedback{margin:2em 0 1em;text-align:center}.md-feedback fieldset{border:none;margin:0;padding:0}.md-feedback__title{font-weight:700;margin:1em auto}.md-feedback__inner{position:relative}.md-feedback__list{display:flex;flex-wrap:wrap;place-content:baseline center;position:relative}.md-feedback__list:hover .md-icon:not(:disabled){color:var(--md-default-fg-color--lighter)}:disabled .md-feedback__list{min-height:1.8rem}.md-feedback__icon{color:var(--md-default-fg-color--light);cursor:pointer;flex-shrink:0;margin:0 .1rem;transition:color 125ms}.md-feedback__icon:not(:disabled).md-icon:hover{color:var(--md-accent-fg-color)}.md-feedback__icon:disabled{color:var(--md-default-fg-color--lightest);pointer-events:none}.md-feedback__note{opacity:0;position:relative;transform:translateY(.4rem);transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .15s}.md-feedback__note>*{margin:0 auto;max-width:16rem}:disabled .md-feedback__note{opacity:1;transform:translateY(0)}@media print{.md-feedback{display:none}}.md-footer{background-color:var(--md-footer-bg-color);color:var(--md-footer-fg-color)}@media print{.md-footer{display:none}}.md-footer__inner{justify-content:space-between;overflow:auto;padding:.2rem}.md-footer__inner:not([hidden]){display:flex}.md-footer__link{align-items:end;display:flex;flex-grow:0.01;margin-bottom:.4rem;margin-top:1rem;max-width:100%;outline-color:var(--md-accent-fg-color);overflow:hidden;transition:opacity .25s}.md-footer__link:focus,.md-footer__link:hover{opacity:.7}[dir=rtl] .md-footer__link svg{transform:scaleX(-1)}@media screen and (max-width:44.984375em){.md-footer__link--prev{flex-shrink:0}.md-footer__link--prev .md-footer__title{display:none}}[dir=ltr] .md-footer__link--next{margin-left:auto}[dir=rtl] .md-footer__link--next{margin-right:auto}.md-footer__link--next{text-align:right}[dir=rtl] .md-footer__link--next{text-align:left}.md-footer__title{flex-grow:1;font-size:.9rem;margin-bottom:.7rem;max-width:calc(100% - 2.4rem);padding:0 1rem;white-space:nowrap}.md-footer__button{margin:.2rem;padding:.4rem}.md-footer__direction{font-size:.64rem;opacity:.7}.md-footer-meta{background-color:var(--md-footer-bg-color--dark)}.md-footer-meta__inner{display:flex;flex-wrap:wrap;justify-content:space-between;padding:.2rem}html .md-footer-meta.md-typeset a{color:var(--md-footer-fg-color--light)}html .md-footer-meta.md-typeset a:focus,html .md-footer-meta.md-typeset a:hover{color:var(--md-footer-fg-color)}.md-copyright{color:var(--md-footer-fg-color--lighter);font-size:.64rem;margin:auto .6rem;padding:.4rem 0;width:100%}@media screen and (min-width:45em){.md-copyright{width:auto}}.md-copyright__highlight{color:var(--md-footer-fg-color--light)}.md-social{display:inline-flex;gap:.2rem;margin:0 .4rem;padding:.2rem 0 .6rem}@media screen and (min-width:45em){.md-social{padding:.6rem 0}}.md-social__link{display:inline-block;height:1.6rem;text-align:center;width:1.6rem}.md-social__link:before{line-height:1.9}.md-social__link svg{fill:currentcolor;max-height:.8rem;vertical-align:-25%}.md-social__link svg.lucide{fill:#0000;stroke:currentcolor}.md-typeset .md-button{border:.1rem solid;border-radius:.1rem;color:var(--md-primary-fg-color);cursor:pointer;display:inline-block;font-weight:700;padding:.625em 2em;transition:color 125ms,background-color 125ms,border-color 125ms}.md-typeset .md-button--primary{background-color:var(--md-primary-fg-color);border-color:var(--md-primary-fg-color);color:var(--md-primary-bg-color)}.md-typeset .md-button:focus,.md-typeset .md-button:hover{background-color:var(--md-accent-fg-color);border-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}[dir=ltr] .md-typeset .md-input{border-top-left-radius:.1rem}[dir=ltr] .md-typeset .md-input,[dir=rtl] .md-typeset .md-input{border-top-right-radius:.1rem}[dir=rtl] .md-typeset .md-input{border-top-left-radius:.1rem}.md-typeset .md-input{border-bottom:.1rem solid var(--md-default-fg-color--lighter);box-shadow:var(--md-shadow-z1);font-size:.8rem;height:1.8rem;padding:0 .6rem;transition:border .25s,box-shadow .25s}.md-typeset .md-input:focus,.md-typeset .md-input:hover{border-bottom-color:var(--md-accent-fg-color);box-shadow:var(--md-shadow-z2)}.md-typeset .md-input--stretch{width:100%}.md-header{background-color:var(--md-primary-fg-color);box-shadow:0 0 .2rem #0000,0 .2rem .4rem #0000;color:var(--md-primary-bg-color);display:block;left:0;position:sticky;right:0;top:0;z-index:4}@media print{.md-header{display:none}}.md-header[hidden]{transform:translateY(-100%);transition:transform .25s cubic-bezier(.8,0,.6,1),box-shadow .25s}.md-header--shadow{box-shadow:0 0 .2rem #0000001a,0 .2rem .4rem #0003;transition:transform .25s cubic-bezier(.1,.7,.1,1),box-shadow .25s}.md-header__inner{align-items:center;display:flex;padding:0 .2rem}.md-header__button{color:currentcolor;cursor:pointer;margin:.2rem;outline-color:var(--md-accent-fg-color);padding:.4rem;position:relative;transition:opacity .25s;vertical-align:middle;z-index:1}.md-header__button:hover{opacity:.7}.md-header__button:not([hidden]){display:inline-block}.md-header__button:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}.md-header__button.md-logo{margin:.2rem;padding:.4rem}@media screen and (max-width:76.234375em){.md-header__button.md-logo{display:none}}.md-header__button.md-logo img,.md-header__button.md-logo svg{fill:currentcolor;display:block;height:1.2rem;width:auto}@media screen and (min-width:60em){.md-header__button[for=__search]{display:none}}.no-js .md-header__button[for=__search]{display:none}[dir=rtl] .md-header__button[for=__search] svg{transform:scaleX(-1)}@media screen and (min-width:76.25em){.md-header__button[for=__drawer]{display:none}}.md-header__topic{display:flex;max-width:100%;position:absolute;transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .15s;white-space:nowrap}.md-header__topic+.md-header__topic{opacity:0;pointer-events:none;transform:translateX(1.25rem);transition:transform .4s cubic-bezier(1,.7,.1,.1),opacity .15s;z-index:-1}[dir=rtl] .md-header__topic+.md-header__topic{transform:translateX(-1.25rem)}.md-header__topic:first-child{font-weight:700}[dir=ltr] .md-header__title{margin-left:1rem;margin-right:.4rem}[dir=rtl] .md-header__title{margin-left:.4rem;margin-right:1rem}.md-header__title{flex-grow:1;font-size:.9rem;height:2.4rem;line-height:2.4rem}.md-header__title--active .md-header__topic{opacity:0;pointer-events:none;transform:translateX(-1.25rem);transition:transform .4s cubic-bezier(1,.7,.1,.1),opacity .15s;z-index:-1}[dir=rtl] .md-header__title--active .md-header__topic{transform:translateX(1.25rem)}.md-header__title--active .md-header__topic+.md-header__topic{opacity:1;pointer-events:auto;transform:translateX(0);transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .15s;z-index:0}.md-header__title>.md-header__ellipsis{height:100%;position:relative;width:100%}.md-header__option{display:flex;flex-shrink:0;max-width:100%;white-space:nowrap}.md-header__option>input{bottom:0}.md-header__source{display:none}@media screen and (min-width:60em){[dir=ltr] .md-header__source{margin-left:1rem}[dir=rtl] .md-header__source{margin-right:1rem}.md-header__source{display:block;max-width:11.7rem;width:11.7rem}}@media screen and (min-width:76.25em){[dir=ltr] .md-header__source{margin-left:1.4rem}[dir=rtl] .md-header__source{margin-right:1.4rem}}.md-meta{color:var(--md-default-fg-color--light);font-size:.7rem;line-height:1.3}.md-meta__list{display:inline-flex;flex-wrap:wrap;list-style:none;margin:0;padding:0}.md-meta__item:not(:last-child):after{content:"·";margin-left:.2rem;margin-right:.2rem}.md-meta__link{color:var(--md-typeset-a-color)}.md-meta__link:focus,.md-meta__link:hover{color:var(--md-accent-fg-color)}.md-draft{background-color:#ff1744;border-radius:.125em;color:#fff;display:inline-block;font-weight:700;padding-left:.5714285714em;padding-right:.5714285714em}:root{--md-nav-icon--prev:url('data:image/svg+xml;charset=utf-8, ');--md-nav-icon--next:url('data:image/svg+xml;charset=utf-8, ');--md-toc-icon:url('data:image/svg+xml;charset=utf-8, ')}.md-nav{font-size:.7rem;line-height:1.3}.md-nav__title{color:var(--md-default-fg-color--light);display:block;font-weight:700;overflow:hidden;padding:0 .6rem;text-overflow:ellipsis}.md-nav__title .md-nav__button{display:none}.md-nav__title .md-nav__button img{height:100%;width:auto}.md-nav__title .md-nav__button.md-logo img,.md-nav__title .md-nav__button.md-logo svg{fill:currentcolor;display:block;height:2.4rem;max-width:100%;object-fit:contain;width:auto}.md-nav__list{list-style:none;margin:0;padding:0}.md-nav__link{align-items:flex-start;display:flex;gap:.4rem;margin-top:.625em;scroll-snap-align:start;transition:color 125ms}.md-nav__link--passed,.md-nav__link--passed code{color:var(--md-default-fg-color--light)}.md-nav__item .md-nav__link--active,.md-nav__item .md-nav__link--active code{color:var(--md-typeset-a-color)}.md-nav__link .md-ellipsis{position:relative}.md-nav__link .md-ellipsis code{word-break:normal}[dir=ltr] .md-nav__link .md-icon:last-child{margin-left:auto}[dir=rtl] .md-nav__link .md-icon:last-child{margin-right:auto}.md-nav__link .md-typeset{font-size:.7rem;line-height:1.3}.md-nav__link svg{fill:currentcolor;flex-shrink:0;height:1.3em;position:relative;width:1.3em}.md-nav__link svg.lucide{fill:#0000;stroke:currentcolor}.md-nav__link[for]:focus,.md-nav__link[for]:hover,.md-nav__link[href]:focus,.md-nav__link[href]:hover{color:var(--md-accent-fg-color);cursor:pointer}.md-nav__link[for]:focus code,.md-nav__link[for]:hover code,.md-nav__link[href]:focus code,.md-nav__link[href]:hover code{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-nav__link.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-nav--primary .md-nav__link[for=__toc]{display:none}.md-nav--primary .md-nav__link[for=__toc] .md-icon:after{background-color:currentcolor;display:block;height:100%;-webkit-mask-image:var(--md-toc-icon);mask-image:var(--md-toc-icon);width:100%}.md-nav--primary .md-nav__link[for=__toc]~.md-nav{display:none}.md-nav__container>.md-nav__link{margin-top:0}.md-nav__container>.md-nav__link:first-child{flex-grow:1;min-width:0}.md-nav__icon{flex-shrink:0}.md-nav__source{display:none}@media screen and (max-width:76.234375em){.md-nav--primary,.md-nav--primary .md-nav{background-color:var(--md-default-bg-color);display:flex;flex-direction:column;height:100%;left:0;position:absolute;right:0;top:0;z-index:1}.md-nav--primary .md-nav__item,.md-nav--primary .md-nav__title{font-size:.8rem;line-height:1.5}.md-nav--primary .md-nav__title{background-color:var(--md-default-fg-color--lightest);color:var(--md-default-fg-color--light);cursor:pointer;height:5.6rem;line-height:2.4rem;padding:3rem .8rem .2rem;position:relative;white-space:nowrap}[dir=ltr] .md-nav--primary .md-nav__title .md-nav__icon{left:.4rem}[dir=rtl] .md-nav--primary .md-nav__title .md-nav__icon{right:.4rem}.md-nav--primary .md-nav__title .md-nav__icon{display:block;height:1.2rem;margin:.2rem;position:absolute;top:.4rem;width:1.2rem}.md-nav--primary .md-nav__title .md-nav__icon:after{background-color:currentcolor;content:"";display:block;height:100%;-webkit-mask-image:var(--md-nav-icon--prev);mask-image:var(--md-nav-icon--prev);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:100%}.md-nav--primary .md-nav__title~.md-nav__list{background-color:var(--md-default-bg-color);box-shadow:0 .05rem 0 var(--md-default-fg-color--lightest) inset;overflow-y:auto;overscroll-behavior-y:contain;scroll-snap-type:y mandatory;touch-action:pan-y}.md-nav--primary .md-nav__title~.md-nav__list>:first-child{border-top:0}.md-nav--primary .md-nav__title[for=__drawer]{background-color:var(--md-primary-fg-color);color:var(--md-primary-bg-color);font-weight:700}.md-nav--primary .md-nav__title .md-logo{display:block;left:.2rem;margin:.2rem;padding:.4rem;position:absolute;right:.2rem;top:.2rem}.md-nav--primary .md-nav__list{flex:1}.md-nav--primary .md-nav__item{border-top:.05rem solid var(--md-default-fg-color--lightest)}.md-nav--primary .md-nav__item--active>.md-nav__link{color:var(--md-typeset-a-color)}.md-nav--primary .md-nav__item--active>.md-nav__link:focus,.md-nav--primary .md-nav__item--active>.md-nav__link:hover{color:var(--md-accent-fg-color)}.md-nav--primary .md-nav__link{margin-top:0;padding:.6rem .8rem}.md-nav--primary .md-nav__link svg{margin-top:.1em}.md-nav--primary .md-nav__link>.md-nav__link{padding:0}[dir=ltr] .md-nav--primary .md-nav__link .md-nav__icon{margin-right:-.2rem}[dir=rtl] .md-nav--primary .md-nav__link .md-nav__icon{margin-left:-.2rem}.md-nav--primary .md-nav__link .md-nav__icon{font-size:1.2rem;height:1.2rem;width:1.2rem}.md-nav--primary .md-nav__link .md-nav__icon:after{background-color:currentcolor;content:"";display:block;height:100%;-webkit-mask-image:var(--md-nav-icon--next);mask-image:var(--md-nav-icon--next);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:100%}[dir=rtl] .md-nav--primary .md-nav__icon:after{transform:scale(-1)}.md-nav--primary .md-nav--secondary .md-nav{background-color:initial;position:static}[dir=ltr] .md-nav--primary .md-nav--secondary .md-nav .md-nav__link{padding-left:1.4rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav__link{padding-right:1.4rem}[dir=ltr] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav__link{padding-left:2rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav__link{padding-right:2rem}[dir=ltr] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav__link{padding-left:2.6rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav__link{padding-right:2.6rem}[dir=ltr] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav .md-nav__link{padding-left:3.2rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav .md-nav__link{padding-right:3.2rem}.md-nav--secondary{background-color:initial}.md-nav__toggle~.md-nav{display:flex;opacity:0;transform:translateX(100%);transition:transform .25s cubic-bezier(.8,0,.6,1),opacity 125ms 50ms}[dir=rtl] .md-nav__toggle~.md-nav{transform:translateX(-100%)}.md-nav__toggle:checked~.md-nav{opacity:1;transform:translateX(0);transition:transform .25s cubic-bezier(.4,0,.2,1),opacity 125ms 125ms}.md-nav__toggle:checked~.md-nav>.md-nav__list{backface-visibility:hidden}}@media screen and (max-width:59.984375em){.md-nav--primary .md-nav__link[for=__toc]{display:flex}.md-nav--primary .md-nav__link[for=__toc] .md-icon:after{content:""}.md-nav--primary .md-nav__link[for=__toc]+.md-nav__link{display:none}.md-nav--primary .md-nav__link[for=__toc]~.md-nav{display:flex}.md-nav__source{background-color:var(--md-primary-fg-color--dark);color:var(--md-primary-bg-color);display:block;padding:0 .2rem}}@media screen and (min-width:60em) and (max-width:76.234375em){.md-nav--integrated .md-nav__link[for=__toc]{display:flex}.md-nav--integrated .md-nav__link[for=__toc] .md-icon:after{content:""}.md-nav--integrated .md-nav__link[for=__toc]+.md-nav__link{display:none}.md-nav--integrated .md-nav__link[for=__toc]~.md-nav{display:flex}}@media screen and (min-width:60em){.md-nav{margin-bottom:-.4rem}.md-nav--secondary .md-nav__title{background:var(--md-default-bg-color);box-shadow:0 0 .4rem .4rem var(--md-default-bg-color);position:sticky;top:0;z-index:1}.md-nav--secondary .md-nav__title[for=__toc]{scroll-snap-align:start}.md-nav--secondary .md-nav__title .md-nav__icon{display:none}[dir=ltr] .md-nav--secondary .md-nav__list{padding-left:.6rem}[dir=rtl] .md-nav--secondary .md-nav__list{padding-right:.6rem}.md-nav--secondary .md-nav__list{padding-bottom:.4rem}[dir=ltr] .md-nav--secondary .md-nav__item>.md-nav__link{margin-right:.4rem}[dir=rtl] .md-nav--secondary .md-nav__item>.md-nav__link{margin-left:.4rem}}@media screen and (min-width:76.25em){.md-nav{margin-bottom:-.4rem;transition:max-height .25s cubic-bezier(.86,0,.07,1)}.md-nav--primary .md-nav__title{background:var(--md-default-bg-color);box-shadow:0 0 .4rem .4rem var(--md-default-bg-color);position:sticky;top:0;z-index:1}.md-nav--primary .md-nav__title[for=__drawer]{scroll-snap-align:start}.md-nav--primary .md-nav__title .md-nav__icon{display:none}[dir=ltr] .md-nav--primary .md-nav__list{padding-left:.6rem}[dir=rtl] .md-nav--primary .md-nav__list{padding-right:.6rem}.md-nav--primary .md-nav__list{padding-bottom:.4rem}[dir=ltr] .md-nav--primary .md-nav__item>.md-nav__link{margin-right:.4rem}[dir=rtl] .md-nav--primary .md-nav__item>.md-nav__link{margin-left:.4rem}.md-nav__toggle~.md-nav{display:grid;grid-template-rows:minmax(.4rem,0fr);opacity:0;transition:grid-template-rows .25s cubic-bezier(.86,0,.07,1),opacity .25s,visibility 0ms .25s;visibility:collapse}.md-nav__toggle~.md-nav>.md-nav__list{overflow:hidden}.md-nav__toggle.md-toggle--indeterminate~.md-nav,.md-nav__toggle:checked~.md-nav{grid-template-rows:minmax(.4rem,1fr);opacity:1;transition:grid-template-rows .25s cubic-bezier(.86,0,.07,1),opacity .15s .1s,visibility 0ms;visibility:visible}.md-nav__toggle.md-toggle--indeterminate~.md-nav{transition:none}.md-nav__item--nested>.md-nav>.md-nav__title{display:none}.md-nav__item--section{display:block;margin:1.25em 0}.md-nav__item--section:last-child{margin-bottom:0}.md-nav__item--section>.md-nav__link{font-weight:700}.md-nav__item--section>.md-nav__link[for]{color:var(--md-default-fg-color--light)}.md-nav__item--section>.md-nav__link:not(.md-nav__container){pointer-events:none}.md-nav__item--section>.md-nav__link .md-icon,.md-nav__item--section>.md-nav__link>[for]{display:none}[dir=ltr] .md-nav__item--section>.md-nav{margin-left:-.6rem}[dir=rtl] .md-nav__item--section>.md-nav{margin-right:-.6rem}.md-nav__item--section>.md-nav{display:block;opacity:1;visibility:visible}.md-nav__item--section>.md-nav>.md-nav__list>.md-nav__item{padding:0}.md-nav__icon{border-radius:100%;height:.9rem;transition:background-color .25s;width:.9rem}.md-nav__icon:hover{background-color:var(--md-accent-fg-color--transparent)}.md-nav__icon:after{background-color:currentcolor;border-radius:100%;content:"";display:inline-block;height:100%;-webkit-mask-image:var(--md-nav-icon--next);mask-image:var(--md-nav-icon--next);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:transform .25s;vertical-align:-.1rem;width:100%}[dir=rtl] .md-nav__icon:after{transform:rotate(180deg)}.md-nav__item--nested .md-nav__toggle:checked~.md-nav__link .md-nav__icon:after,.md-nav__item--nested .md-toggle--indeterminate~.md-nav__link .md-nav__icon:after{transform:rotate(90deg)}.md-nav--lifted>.md-nav__list>.md-nav__item,.md-nav--lifted>.md-nav__title{display:none}.md-nav--lifted>.md-nav__list>.md-nav__item--active{display:block}.md-nav--lifted>.md-nav__list>.md-nav__item--active>.md-nav__link{background:var(--md-default-bg-color);box-shadow:0 0 .4rem .4rem var(--md-default-bg-color);margin-top:0;position:sticky;top:0;z-index:1}.md-nav--lifted>.md-nav__list>.md-nav__item--active>.md-nav__link:not(.md-nav__container){pointer-events:none}.md-nav--lifted>.md-nav__list>.md-nav__item--active.md-nav__item--section{margin:0}[dir=ltr] .md-nav--lifted>.md-nav__list>.md-nav__item>.md-nav:not(.md-nav--secondary){margin-left:-.6rem}[dir=rtl] .md-nav--lifted>.md-nav__list>.md-nav__item>.md-nav:not(.md-nav--secondary){margin-right:-.6rem}.md-nav--lifted>.md-nav__list>.md-nav__item>[for]{color:var(--md-default-fg-color--light)}.md-nav--lifted .md-nav[data-md-level="1"]{grid-template-rows:minmax(.4rem,1fr);opacity:1;visibility:visible}[dir=ltr] .md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{border-left:.05rem solid var(--md-primary-fg-color)}[dir=rtl] .md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{border-right:.05rem solid var(--md-primary-fg-color)}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{display:block;margin-bottom:1.25em;opacity:1;visibility:visible}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary>.md-nav__list{overflow:visible;padding-bottom:0}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary>.md-nav__title{display:none}}.md-pagination{font-size:.8rem;font-weight:700;gap:.4rem}.md-pagination,.md-pagination>*{align-items:center;display:flex;justify-content:center}.md-pagination>*{border-radius:.2rem;height:1.8rem;min-width:1.8rem;text-align:center}.md-pagination__current{background-color:var(--md-default-fg-color--lightest);color:var(--md-default-fg-color--light)}.md-pagination__link{transition:color 125ms,background-color 125ms}.md-pagination__link:focus,.md-pagination__link:hover{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-pagination__link:focus svg,.md-pagination__link:hover svg{color:var(--md-accent-fg-color)}.md-pagination__link.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-pagination__link svg{fill:currentcolor;color:var(--md-default-fg-color--lighter);display:block;max-height:100%;width:1.2rem}:root{--md-path-icon:url('data:image/svg+xml;charset=utf-8, ')}.md-path{font-size:.7rem;margin:0 .8rem;overflow:auto;padding-top:1.2rem}.md-path:not([hidden]){display:block}@media screen and (min-width:76.25em){.md-path{margin:0 1.2rem}}.md-path__list{align-items:center;display:flex;gap:.2rem;list-style:none;margin:0;padding:0}.md-path__item:not(:first-child){display:inline-flex;gap:.2rem;white-space:nowrap}.md-path__item:not(:first-child):before{background-color:var(--md-default-fg-color--lighter);content:"";display:inline;height:.8rem;-webkit-mask-image:var(--md-path-icon);mask-image:var(--md-path-icon);width:.8rem}.md-path__link{align-items:center;color:var(--md-default-fg-color--light);display:flex}.md-path__link:focus,.md-path__link:hover{color:var(--md-accent-fg-color)}:root{--md-post-pin-icon:url('data:image/svg+xml;charset=utf-8, ')}.md-post__back{border-bottom:.05rem solid var(--md-default-fg-color--lightest);margin-bottom:1.2rem;padding-bottom:1.2rem}@media screen and (max-width:76.234375em){.md-post__back{display:none}}[dir=rtl] .md-post__back svg{transform:scaleX(-1)}.md-post__authors{display:flex;flex-direction:column;gap:.6rem;margin:0 .6rem 1.2rem}.md-post .md-post__meta a{transition:color 125ms}.md-post .md-post__meta a:focus,.md-post .md-post__meta a:hover{color:var(--md-accent-fg-color)}.md-post__title{color:var(--md-default-fg-color--light);font-weight:700}.md-post--excerpt{margin-bottom:3.2rem}.md-post--excerpt .md-post__header{align-items:center;display:flex;gap:.6rem;min-height:1.6rem}.md-post--excerpt .md-post__authors{align-items:center;display:inline-flex;flex-direction:row;gap:.2rem;margin:0;min-height:2.4rem}[dir=ltr] .md-post--excerpt .md-post__meta .md-meta__list{margin-right:.4rem}[dir=rtl] .md-post--excerpt .md-post__meta .md-meta__list{margin-left:.4rem}.md-post--excerpt .md-post__content>:first-child{--md-scroll-margin:6rem;margin-top:0}.md-post>.md-nav--secondary{margin:1em 0}.md-pin{background:var(--md-default-fg-color--lightest);border-radius:1rem;margin-top:-.05rem;padding:.2rem}.md-pin:after{background-color:currentcolor;content:"";display:block;height:.6rem;margin:0 auto;-webkit-mask-image:var(--md-post-pin-icon);mask-image:var(--md-post-pin-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:.6rem}.md-profile{align-items:center;display:flex;font-size:.7rem;gap:.6rem;line-height:1.4;width:100%}.md-profile__description{flex-grow:1}.md-content--post{display:flex}@media screen and (max-width:76.234375em){.md-content--post{flex-flow:column-reverse}}.md-content--post>.md-content__inner{flex-grow:1;min-width:0}@media screen and (min-width:76.25em){[dir=ltr] .md-content--post>.md-content__inner{margin-left:1.2rem}[dir=rtl] .md-content--post>.md-content__inner{margin-right:1.2rem}}@media screen and (max-width:76.234375em){.md-sidebar.md-sidebar--post{padding:0;position:static;width:100%}.md-sidebar.md-sidebar--post .md-sidebar__scrollwrap{overflow:visible}.md-sidebar.md-sidebar--post .md-sidebar__inner{padding:0}.md-sidebar.md-sidebar--post .md-post__meta{margin-left:.6rem;margin-right:.6rem}.md-sidebar.md-sidebar--post .md-nav__item{border:none;display:inline}.md-sidebar.md-sidebar--post .md-nav__list{display:inline-flex;flex-wrap:wrap;gap:.6rem;padding-bottom:.6rem;padding-top:.6rem}.md-sidebar.md-sidebar--post .md-nav__link{padding:0}.md-sidebar.md-sidebar--post .md-nav{height:auto;margin-bottom:0;position:static}}:root{--md-progress-value:0;--md-progress-delay:400ms}.md-progress{background:var(--md-primary-bg-color);height:.075rem;opacity:min(clamp(0,var(--md-progress-value),1),clamp(0,100 - var(--md-progress-value),1));position:fixed;top:0;transform:scaleX(calc(var(--md-progress-value)*1%));transform-origin:left;transition:transform .5s cubic-bezier(.19,1,.22,1),opacity .25s var(--md-progress-delay);width:100%;z-index:4}:root{--md-search-icon:url('data:image/svg+xml;charset=utf-8, ')}.md-search{position:relative}@media screen and (min-width:60em){.md-search{padding:.2rem 0}}@media screen and (max-width:59.984375em){.md-search{display:none}}.no-js .md-search{display:none}[dir=ltr] .md-search__button{padding-left:1.9rem;padding-right:2.2rem}[dir=rtl] .md-search__button{padding-left:2.2rem;padding-right:1.9rem}.md-search__button{background:var(--md-primary-fg-color);color:var(--md-primary-bg-color);cursor:pointer;font-size:.7rem;position:relative;text-align:left}@media screen and (min-width:45em){.md-search__button{background-color:#00000042;border-radius:.2rem;height:1.6rem;transition:background-color .4s,color .4s;width:8.9rem}.md-search__button:focus,.md-search__button:hover{background-color:#ffffff1f;color:var(--md-primary-bg-color)}}[dir=ltr] .md-search__button:before{left:0}[dir=rtl] .md-search__button:before{right:0}.md-search__button:before{background-color:var(--md-primary-bg-color);content:"";height:1rem;margin-left:.5rem;-webkit-mask-image:var(--md-search-icon);mask-image:var(--md-search-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.3rem;width:1rem}.md-search__button:after{background:#00000042;border-radius:.1rem;content:"Ctrl+K";display:block;font-size:.6rem;padding:.1rem .2rem;position:absolute;right:.6rem;top:.35rem}[data-platform^=Mac] .md-search__button:after{content:"⌘K"}.md-select{position:relative;z-index:1}.md-select__inner{background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color);left:50%;margin-top:.2rem;max-height:0;opacity:0;position:absolute;top:calc(100% - .2rem);transform:translate3d(-50%,.3rem,0);transition:transform .25s 375ms,opacity .25s .25s,max-height 0ms .5s}@media screen and (max-width:59.984375em){.md-select__inner{left:100%;transform:translate3d(-100%,.3rem,0)}}.md-select:focus-within .md-select__inner,.md-select:hover .md-select__inner{max-height:min(75vh,28rem);opacity:1;transform:translate3d(-50%,0,0);transition:transform .25s cubic-bezier(.1,.7,.1,1),opacity .25s,max-height 0ms}@media screen and (max-width:59.984375em){.md-select:focus-within .md-select__inner,.md-select:hover .md-select__inner{transform:translate3d(-100%,0,0)}}.md-select__inner:after{border-bottom:.2rem solid #0000;border-bottom-color:var(--md-default-bg-color);border-left:.2rem solid #0000;border-right:.2rem solid #0000;border-top:0;content:"";filter:drop-shadow(0 -1px 0 var(--md-default-fg-color--lightest));height:0;left:50%;margin-left:-.2rem;margin-top:-.2rem;position:absolute;top:0;width:0}@media screen and (max-width:59.984375em){.md-select__inner:after{left:auto;right:1rem}}.md-select__list{border-radius:.1rem;font-size:.8rem;list-style-type:none;margin:0;max-height:inherit;overflow:auto;padding:0}.md-select__item{line-height:1.8rem}[dir=ltr] .md-select__link{padding-left:.6rem;padding-right:1.2rem}[dir=rtl] .md-select__link{padding-left:1.2rem;padding-right:.6rem}.md-select__link{cursor:pointer;display:block;outline:none;scroll-snap-align:start;transition:background-color .25s,color .25s;width:100%}.md-select__link:focus,.md-select__link:hover{color:var(--md-accent-fg-color)}.md-select__link:focus{background-color:var(--md-default-fg-color--lightest)}.md-sidebar{align-self:flex-start;flex-shrink:0;padding:1.2rem 0;position:sticky;top:2.4rem;width:12.1rem}@media print{.md-sidebar{display:none}}@media screen and (max-width:76.234375em){[dir=ltr] .md-sidebar--primary{left:-12.1rem}[dir=rtl] .md-sidebar--primary{right:-12.1rem}.md-sidebar--primary{background-color:var(--md-default-bg-color);display:block;height:100%;position:fixed;top:0;transform:translateX(0);transition:transform .25s cubic-bezier(.4,0,.2,1),box-shadow .25s;width:12.1rem;z-index:5}[data-md-toggle=drawer]:checked~.md-container .md-sidebar--primary{box-shadow:var(--md-shadow-z3);transform:translateX(12.1rem)}[dir=rtl] [data-md-toggle=drawer]:checked~.md-container .md-sidebar--primary{transform:translateX(-12.1rem)}.md-sidebar--primary .md-sidebar__scrollwrap{bottom:0;left:0;margin:0;overflow:hidden;overscroll-behavior-y:contain;position:absolute;right:0;scroll-snap-type:none;top:0}}@media screen and (min-width:76.25em){.md-sidebar{height:0}.no-js .md-sidebar{height:auto}.md-header--lifted~.md-container .md-sidebar{top:4.8rem}}.md-sidebar--secondary{display:none;order:2}@media screen and (min-width:60em){.md-sidebar--secondary{height:0}.no-js .md-sidebar--secondary{height:auto}.md-sidebar--secondary:not([hidden]){display:block}.md-sidebar--secondary .md-sidebar__scrollwrap{touch-action:pan-y}}.md-sidebar__scrollwrap{backface-visibility:hidden;margin:0 .2rem;overflow-y:auto;scrollbar-color:var(--md-default-fg-color--lighter) #0000}@media screen and (min-width:60em){.md-sidebar__scrollwrap{scrollbar-gutter:stable;scrollbar-width:thin}}.md-sidebar__scrollwrap::-webkit-scrollbar{height:.2rem;width:.2rem}.md-sidebar__scrollwrap:focus-within,.md-sidebar__scrollwrap:hover{scrollbar-color:var(--md-accent-fg-color) #0000}.md-sidebar__scrollwrap:focus-within::-webkit-scrollbar-thumb,.md-sidebar__scrollwrap:hover::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-sidebar__scrollwrap:focus-within::-webkit-scrollbar-thumb:hover,.md-sidebar__scrollwrap:hover::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}@supports selector(::-webkit-scrollbar){.md-sidebar__scrollwrap{scrollbar-gutter:auto}[dir=ltr] .md-sidebar__inner{padding-right:calc(100% - 11.5rem)}[dir=rtl] .md-sidebar__inner{padding-left:calc(100% - 11.5rem)}}@media screen and (max-width:76.234375em){.md-overlay{background-color:#0000008a;height:0;opacity:0;position:fixed;top:0;transition:width 0ms .25s,height 0ms .25s,opacity .25s;width:0;z-index:5}[data-md-toggle=drawer]:checked~.md-overlay{height:100%;opacity:1;transition:width 0ms,height 0ms,opacity .25s;width:100%}}@keyframes facts{0%{height:0}to{height:.65rem}}@keyframes fact{0%{opacity:0;transform:translateY(100%)}50%{opacity:0}to{opacity:1;transform:translateY(0)}}:root{--md-source-forks-icon:url('data:image/svg+xml;charset=utf-8, ');--md-source-repositories-icon:url('data:image/svg+xml;charset=utf-8, ');--md-source-stars-icon:url('data:image/svg+xml;charset=utf-8, ');--md-source-version-icon:url('data:image/svg+xml;charset=utf-8, ')}.md-source{backface-visibility:hidden;display:block;font-size:.65rem;line-height:1.2;outline-color:var(--md-accent-fg-color);transition:opacity .25s;white-space:nowrap}.md-source:hover{opacity:.7}.md-source__icon{display:inline-block;height:2.4rem;vertical-align:middle;width:2rem}[dir=ltr] .md-source__icon svg{margin-left:.6rem}[dir=rtl] .md-source__icon svg{margin-right:.6rem}.md-source__icon svg{margin-top:.6rem}[dir=ltr] .md-source__icon+.md-source__repository{padding-left:2rem}[dir=rtl] .md-source__icon+.md-source__repository{padding-right:2rem}[dir=ltr] .md-source__icon+.md-source__repository{margin-left:-2rem}[dir=rtl] .md-source__icon+.md-source__repository{margin-right:-2rem}[dir=ltr] .md-source__repository{margin-left:.6rem}[dir=rtl] .md-source__repository{margin-right:.6rem}.md-source__repository{display:inline-block;max-width:calc(100% - 1.2rem);overflow:hidden;text-overflow:ellipsis;vertical-align:middle}.md-source__facts{display:flex;font-size:.55rem;gap:.4rem;list-style-type:none;margin:.1rem 0 0;opacity:.75;overflow:hidden;padding:0;width:100%}.md-source__repository--active .md-source__facts{animation:facts .25s ease-in}.md-source__fact{overflow:hidden;text-overflow:ellipsis}.md-source__repository--active .md-source__fact{animation:fact .4s ease-out}[dir=ltr] .md-source__fact:before{margin-right:.1rem}[dir=rtl] .md-source__fact:before{margin-left:.1rem}.md-source__fact:before{background-color:currentcolor;content:"";display:inline-block;height:.6rem;-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;vertical-align:text-top;width:.6rem}.md-source__fact:nth-child(1n+2){flex-shrink:0}.md-source__fact--version:before{-webkit-mask-image:var(--md-source-version-icon);mask-image:var(--md-source-version-icon)}.md-source__fact--stars:before{-webkit-mask-image:var(--md-source-stars-icon);mask-image:var(--md-source-stars-icon)}.md-source__fact--forks:before{-webkit-mask-image:var(--md-source-forks-icon);mask-image:var(--md-source-forks-icon)}.md-source__fact--repositories:before{-webkit-mask-image:var(--md-source-repositories-icon);mask-image:var(--md-source-repositories-icon)}.md-source-file{margin:1em 0}[dir=ltr] .md-source-file__fact{margin-right:.6rem}[dir=rtl] .md-source-file__fact{margin-left:.6rem}.md-source-file__fact{align-items:center;color:var(--md-default-fg-color--light);display:inline-flex;font-size:.68rem;gap:.3rem}.md-source-file__fact .md-icon{flex-shrink:0;margin-bottom:.05rem}[dir=ltr] .md-source-file__fact .md-author{float:left}[dir=rtl] .md-source-file__fact .md-author{float:right}.md-source-file__fact .md-author{margin-right:.2rem}.md-source-file__fact svg{width:.9rem}:root{--md-status:url('data:image/svg+xml;charset=utf-8, ');--md-status--new:url('data:image/svg+xml;charset=utf-8, ');--md-status--deprecated:url('data:image/svg+xml;charset=utf-8, ');--md-status--encrypted:url('data:image/svg+xml;charset=utf-8, ')}.md-status:after{background-color:var(--md-default-fg-color--light);content:"";display:inline-block;height:1.125em;-webkit-mask-image:var(--md-status);mask-image:var(--md-status);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;vertical-align:text-bottom;width:1.125em}.md-status:hover:after{background-color:currentcolor}.md-status--new:after{-webkit-mask-image:var(--md-status--new);mask-image:var(--md-status--new)}.md-status--deprecated:after{-webkit-mask-image:var(--md-status--deprecated);mask-image:var(--md-status--deprecated)}.md-status--encrypted:after{-webkit-mask-image:var(--md-status--encrypted);mask-image:var(--md-status--encrypted)}.md-tabs{background-color:var(--md-primary-fg-color);color:var(--md-primary-bg-color);display:block;line-height:1.3;overflow:auto;width:100%;z-index:3}@media print{.md-tabs{display:none}}@media screen and (max-width:76.234375em){.md-tabs{display:none}}.md-tabs[hidden]{pointer-events:none}[dir=ltr] .md-tabs__list{margin-left:.2rem}[dir=rtl] .md-tabs__list{margin-right:.2rem}.md-tabs__list{contain:content;display:flex;list-style:none;margin:0;overflow:auto;padding:0;scrollbar-width:none;white-space:nowrap}.md-tabs__list::-webkit-scrollbar{display:none}.md-tabs__item{height:2.4rem;padding-left:.6rem;padding-right:.6rem}.md-tabs__item--active .md-tabs__link{color:inherit;opacity:1}.md-tabs__link{backface-visibility:hidden;display:flex;font-size:.7rem;margin-top:.8rem;opacity:.7;outline-color:var(--md-accent-fg-color);outline-offset:.2rem;transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .25s}.md-tabs__link:focus,.md-tabs__link:hover{color:inherit;opacity:1}[dir=ltr] .md-tabs__link svg{margin-right:.4rem}[dir=rtl] .md-tabs__link svg{margin-left:.4rem}.md-tabs__link svg{fill:currentcolor;height:1.3em}.md-tabs__item:nth-child(2) .md-tabs__link{transition-delay:20ms}.md-tabs__item:nth-child(3) .md-tabs__link{transition-delay:40ms}.md-tabs__item:nth-child(4) .md-tabs__link{transition-delay:60ms}.md-tabs__item:nth-child(5) .md-tabs__link{transition-delay:80ms}.md-tabs__item:nth-child(6) .md-tabs__link{transition-delay:.1s}.md-tabs__item:nth-child(7) .md-tabs__link{transition-delay:.12s}.md-tabs__item:nth-child(8) .md-tabs__link{transition-delay:.14s}.md-tabs__item:nth-child(9) .md-tabs__link{transition-delay:.16s}.md-tabs__item:nth-child(10) .md-tabs__link{transition-delay:.18s}.md-tabs__item:nth-child(11) .md-tabs__link{transition-delay:.2s}.md-tabs__item:nth-child(12) .md-tabs__link{transition-delay:.22s}.md-tabs__item:nth-child(13) .md-tabs__link{transition-delay:.24s}.md-tabs__item:nth-child(14) .md-tabs__link{transition-delay:.26s}.md-tabs__item:nth-child(15) .md-tabs__link{transition-delay:.28s}.md-tabs__item:nth-child(16) .md-tabs__link{transition-delay:.3s}.md-tabs[hidden] .md-tabs__link{opacity:0;transform:translateY(50%);transition:transform 0ms .1s,opacity .1s}:root{--md-tag-icon:url('data:image/svg+xml;charset=utf-8, ')}.md-typeset .md-tags:not([hidden]){display:inline-flex;flex-wrap:wrap;gap:.5em;margin-bottom:.75em;margin-top:-.125em}.md-typeset .md-tag{align-items:center;background:var(--md-default-fg-color--lightest);border-radius:2.4rem;display:inline-flex;font-size:.64rem;font-size:min(.8em,.64rem);font-weight:700;gap:.5em;letter-spacing:normal;line-height:1.6;padding:.3125em .78125em}.md-typeset .md-tag[href]{-webkit-tap-highlight-color:transparent;color:inherit;outline:none;transition:color 125ms,background-color 125ms}.md-typeset .md-tag[href]:focus,.md-typeset .md-tag[href]:hover{background-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}[id]>.md-typeset .md-tag{vertical-align:text-top}.md-typeset .md-tag-shadow{opacity:.5}.md-typeset .md-tag-icon:before{background-color:var(--md-default-fg-color--lighter);content:"";display:inline-block;height:1.2em;-webkit-mask-image:var(--md-tag-icon);mask-image:var(--md-tag-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:background-color 125ms;vertical-align:text-bottom;width:1.2em}.md-typeset .md-tag-icon[href]:focus:before,.md-typeset .md-tag-icon[href]:hover:before{background-color:var(--md-accent-bg-color)}@keyframes pulse{0%{transform:scale(.95)}75%{transform:scale(1)}to{transform:scale(.95)}}:root{--md-annotation-bg-icon:url('data:image/svg+xml;charset=utf-8, ');--md-annotation-icon:url('data:image/svg+xml;charset=utf-8, ')}.md-tooltip{backface-visibility:hidden;background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color);font-family:var(--md-text-font-family);left:clamp(var(--md-tooltip-0,0rem) + .8rem,var(--md-tooltip-x),100vw + var(--md-tooltip-0,0rem) + .8rem - var(--md-tooltip-width) - 2 * .8rem);max-width:calc(100vw - 1.6rem);opacity:0;position:absolute;top:var(--md-tooltip-y);transform:translateY(-.4rem);transition:transform 0ms .25s,opacity .25s,z-index .25s;width:var(--md-tooltip-width);z-index:0}.md-tooltip--active{opacity:1;transform:translateY(0);transition:transform .25s cubic-bezier(.1,.7,.1,1),opacity .25s,z-index 0ms;z-index:2}.md-tooltip--inline{font-weight:700;-webkit-user-select:none;user-select:none;width:auto}.md-tooltip--inline:not(.md-tooltip--active){transform:translateY(.2rem) scale(.9)}.md-tooltip--inline .md-tooltip__inner{font-size:.5rem;padding:.2rem .4rem}[hidden]+.md-tooltip--inline{display:none}.focus-visible>.md-tooltip,.md-tooltip:target{outline:var(--md-accent-fg-color) auto}.md-tooltip__inner{font-size:.64rem;padding:.8rem}.md-tooltip__inner.md-typeset>:first-child{margin-top:0}.md-tooltip__inner.md-typeset>:last-child{margin-bottom:0}.md-annotation{font-style:normal;font-weight:400;outline:none;text-align:initial;vertical-align:text-bottom;white-space:normal}[dir=rtl] .md-annotation{direction:rtl}code .md-annotation{font-family:var(--md-code-font-family);font-size:inherit}.md-annotation:not([hidden]){display:inline-block;line-height:1.25}.md-annotation__index{border-radius:.01px;cursor:pointer;display:inline-block;margin-left:.4ch;margin-right:.4ch;outline:none;overflow:hidden;position:relative;-webkit-user-select:none;user-select:none;vertical-align:text-top;z-index:0}.md-annotation .md-annotation__index{transition:z-index .25s}@media screen{.md-annotation__index{width:2.2ch}[data-md-visible]>.md-annotation__index{animation:pulse 2s infinite}.md-annotation__index:before{background:var(--md-default-bg-color);-webkit-mask-image:var(--md-annotation-bg-icon);mask-image:var(--md-annotation-bg-icon)}.md-annotation__index:after,.md-annotation__index:before{content:"";height:2.2ch;-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:-.1ch;width:2.2ch;z-index:-1}.md-annotation__index:after{background-color:var(--md-default-fg-color--lighter);-webkit-mask-image:var(--md-annotation-icon);mask-image:var(--md-annotation-icon);transform:scale(1.0001);transition:background-color .25s,transform .25s}.md-tooltip--active+.md-annotation__index:after{transform:rotate(45deg)}.md-tooltip--active+.md-annotation__index:after,:hover>.md-annotation__index:after{background-color:var(--md-accent-fg-color)}}.md-tooltip--active+.md-annotation__index{animation-play-state:paused;transition-duration:0ms;z-index:2}.md-annotation__index [data-md-annotation-id]{display:inline-block}@media print{.md-annotation__index [data-md-annotation-id]{background:var(--md-default-fg-color--lighter);border-radius:2ch;color:var(--md-default-bg-color);font-weight:700;padding:0 .6ch;white-space:nowrap}.md-annotation__index [data-md-annotation-id]:after{content:attr(data-md-annotation-id)}}.md-typeset .md-annotation-list{counter-reset:annotation;list-style:none!important}.md-typeset .md-annotation-list li{position:relative}[dir=ltr] .md-typeset .md-annotation-list li:before{left:-2.125em}[dir=rtl] .md-typeset .md-annotation-list li:before{right:-2.125em}.md-typeset .md-annotation-list li:before{background:var(--md-default-fg-color--lighter);border-radius:2ch;color:var(--md-default-bg-color);content:counter(annotation);counter-increment:annotation;font-size:.8875em;font-weight:700;height:2ch;line-height:1.25;min-width:2ch;padding:0 .6ch;position:absolute;text-align:center;top:.25em}:root{--md-tooltip-width:20rem;--md-tooltip-tail:0.3rem}.md-tooltip2{backface-visibility:hidden;color:var(--md-default-fg-color);font-family:var(--md-text-font-family);opacity:0;pointer-events:none;position:absolute;top:calc(var(--md-tooltip-host-y) + var(--md-tooltip-y));transform:translateY(-.4rem);transform-origin:calc(var(--md-tooltip-host-x) + var(--md-tooltip-x)) 0;transition:transform 0ms .25s,opacity .25s,z-index .25s;width:100%;z-index:0}.md-tooltip2:before{border-left:var(--md-tooltip-tail) solid #0000;border-right:var(--md-tooltip-tail) solid #0000;content:"";display:block;left:clamp(1.5 * .8rem,var(--md-tooltip-host-x) + var(--md-tooltip-x) - var(--md-tooltip-tail),100vw - 2 * var(--md-tooltip-tail) - 1.5 * .8rem);position:absolute;z-index:1}.md-tooltip2--top:before{border-top:var(--md-tooltip-tail) solid var(--md-default-bg-color);bottom:calc(var(--md-tooltip-tail)*-1 + .025rem);filter:drop-shadow(0 1px 0 hsla(0,0%,0%,.05))}.md-tooltip2--bottom:before{border-bottom:var(--md-tooltip-tail) solid var(--md-default-bg-color);filter:drop-shadow(0 -1px 0 hsla(0,0%,0%,.05));top:calc(var(--md-tooltip-tail)*-1 + .025rem)}.md-tooltip2--active{opacity:1;transform:translateY(0);transition:transform .4s cubic-bezier(0,1,.5,1),opacity .25s,z-index 0ms;z-index:4}.md-tooltip2__inner{scrollbar-gutter:stable;background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);left:clamp(.8rem,var(--md-tooltip-host-x) - .8rem,100vw - var(--md-tooltip-width) - .8rem);max-height:40vh;max-width:calc(100vw - 1.6rem);position:relative;scrollbar-width:thin}.md-tooltip2__inner::-webkit-scrollbar{height:.2rem;width:.2rem}.md-tooltip2__inner::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-tooltip2__inner::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}[role=dialog]>.md-tooltip2__inner{font-size:.64rem;overflow:auto;padding:0 .8rem;pointer-events:auto;width:var(--md-tooltip-width)}[role=dialog]>.md-tooltip2__inner:after,[role=dialog]>.md-tooltip2__inner:before{content:"";display:block;height:.8rem;position:sticky;width:100%;z-index:10}[role=dialog]>.md-tooltip2__inner:before{background:linear-gradient(var(--md-default-bg-color),#0000 75%);top:0}[role=dialog]>.md-tooltip2__inner:after{background:linear-gradient(#0000,var(--md-default-bg-color) 75%);bottom:0}[role=tooltip]>.md-tooltip2__inner{font-size:.5rem;font-weight:700;left:clamp(.8rem,var(--md-tooltip-host-x) + var(--md-tooltip-x) - var(--md-tooltip-width)/2,100vw - var(--md-tooltip-width) - .8rem);max-width:min(100vw - 2 * .8rem,400px);padding:.2rem .4rem;-webkit-user-select:none;user-select:none;width:fit-content}.md-tooltip2__inner.md-typeset>:first-child{margin-top:0}.md-tooltip2__inner.md-typeset>:last-child{margin-bottom:0}[dir=ltr] .md-top{margin-left:50%}[dir=rtl] .md-top{margin-right:50%}.md-top{background-color:var(--md-default-bg-color);border-radius:1.6rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color--light);cursor:pointer;display:block;font-size:.7rem;outline:none;padding:.4rem .8rem;position:fixed;top:3.2rem;transform:translate(-50%);transition:color 125ms,background-color 125ms,transform 125ms cubic-bezier(.4,0,.2,1),opacity 125ms;z-index:2}@media print{.md-top{display:none}}[dir=rtl] .md-top{transform:translate(50%)}.md-top[hidden]{opacity:0;pointer-events:none;transform:translate(-50%,.2rem);transition-duration:0ms}[dir=rtl] .md-top[hidden]{transform:translate(50%,.2rem)}.md-top:focus,.md-top:hover{background-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}.md-top svg{display:inline-block;vertical-align:-.5em}.md-top.lucide{fill:#0000;stroke:currentcolor}@keyframes hoverfix{0%{pointer-events:none}}:root{--md-version-icon:url('data:image/svg+xml;charset=utf-8, ')}.md-version{flex-shrink:0;font-size:.8rem;height:2.4rem}[dir=ltr] .md-version__current{margin-left:1.4rem;margin-right:.4rem}[dir=rtl] .md-version__current{margin-left:.4rem;margin-right:1.4rem}.md-version__current{color:inherit;cursor:pointer;outline:none;position:relative;top:.05rem}[dir=ltr] .md-version__current:after{margin-left:.4rem}[dir=rtl] .md-version__current:after{margin-right:.4rem}.md-version__current:after{background-color:currentcolor;content:"";display:inline-block;height:.6rem;-webkit-mask-image:var(--md-version-icon);mask-image:var(--md-version-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:.4rem}.md-version__alias{margin-left:.3rem;opacity:.7}.md-version__list{background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color);list-style-type:none;margin:.2rem .8rem;max-height:0;opacity:0;overflow:auto;padding:0;position:absolute;scroll-snap-type:y mandatory;top:.15rem;transition:max-height 0ms .5s,opacity .25s .25s;z-index:3}.md-version:focus-within .md-version__list,.md-version:hover .md-version__list{max-height:10rem;opacity:1;transition:max-height 0ms,opacity .25s}@media (hover:none),(pointer:coarse){.md-version:hover .md-version__list{animation:hoverfix .25s forwards}.md-version:focus-within .md-version__list{animation:none}}.md-version__item{line-height:1.8rem}[dir=ltr] .md-version__link{padding-left:.6rem;padding-right:1.2rem}[dir=rtl] .md-version__link{padding-left:1.2rem;padding-right:.6rem}.md-version__link{cursor:pointer;display:block;outline:none;scroll-snap-align:start;transition:color .25s,background-color .25s;white-space:nowrap;width:100%}.md-version__link:focus,.md-version__link:hover{color:var(--md-accent-fg-color)}.md-version__link:focus{background-color:var(--md-default-fg-color--lightest)}:root{--md-admonition-icon--note:url('data:image/svg+xml;charset=utf-8, ');--md-admonition-icon--abstract:url('data:image/svg+xml;charset=utf-8, ');--md-admonition-icon--info:url('data:image/svg+xml;charset=utf-8, ');--md-admonition-icon--tip:url('data:image/svg+xml;charset=utf-8, ');--md-admonition-icon--success:url('data:image/svg+xml;charset=utf-8, ');--md-admonition-icon--question:url('data:image/svg+xml;charset=utf-8, ');--md-admonition-icon--warning:url('data:image/svg+xml;charset=utf-8, ');--md-admonition-icon--failure:url('data:image/svg+xml;charset=utf-8, ');--md-admonition-icon--danger:url('data:image/svg+xml;charset=utf-8, ');--md-admonition-icon--bug:url('data:image/svg+xml;charset=utf-8, ');--md-admonition-icon--example:url('data:image/svg+xml;charset=utf-8, ');--md-admonition-icon--quote:url('data:image/svg+xml;charset=utf-8, ')}.md-typeset .admonition,.md-typeset details{background-color:var(--md-admonition-bg-color);border:.075rem solid #448aff;border-radius:.2rem;box-shadow:var(--md-shadow-z1);color:var(--md-admonition-fg-color);display:flow-root;font-size:.64rem;margin:1.5625em 0;padding:0 .6rem;page-break-inside:avoid;transition:box-shadow 125ms}@media print{.md-typeset .admonition,.md-typeset details{box-shadow:none}}.md-typeset .admonition:focus-within,.md-typeset details:focus-within{box-shadow:0 0 0 .2rem #448aff1a}.md-typeset .admonition>*,.md-typeset details>*{box-sizing:border-box}.md-typeset .admonition .admonition,.md-typeset .admonition details,.md-typeset details .admonition,.md-typeset details details{margin-bottom:1em;margin-top:1em}.md-typeset .admonition .md-typeset__scrollwrap,.md-typeset details .md-typeset__scrollwrap{margin:1em -.6rem}.md-typeset .admonition .md-typeset__table,.md-typeset details .md-typeset__table{padding:0 .6rem}.md-typeset .admonition>.tabbed-set:only-child,.md-typeset details>.tabbed-set:only-child{margin-top:0}html .md-typeset .admonition>:last-child,html .md-typeset details>:last-child{margin-bottom:.6rem}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary{padding-left:2rem;padding-right:.6rem}[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{padding-left:.6rem;padding-right:2rem}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary{border-left-width:.2rem}[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{border-right-width:.2rem}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary{border-top-left-radius:.1rem}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary,[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{border-top-right-radius:.1rem}[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{border-top-left-radius:.1rem}.md-typeset .admonition-title,.md-typeset summary{background-color:#448aff1a;border:none;font-weight:700;margin:0 -.6rem;padding-bottom:.4rem;padding-top:.4rem;position:relative}html .md-typeset .admonition-title:last-child,html .md-typeset summary:last-child{margin-bottom:0}[dir=ltr] .md-typeset .admonition-title:before,[dir=ltr] .md-typeset summary:before{left:.6rem}[dir=rtl] .md-typeset .admonition-title:before,[dir=rtl] .md-typeset summary:before{right:.6rem}.md-typeset .admonition-title:before,.md-typeset summary:before{background-color:#448aff;content:"";height:1rem;-webkit-mask-image:var(--md-admonition-icon--note);mask-image:var(--md-admonition-icon--note);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.625em;width:1rem}.md-typeset .admonition-title code,.md-typeset summary code{box-shadow:0 0 0 .05rem var(--md-default-fg-color--lightest)}.md-typeset .admonition.note,.md-typeset details.note{border-color:#448aff}.md-typeset .admonition.note:focus-within,.md-typeset details.note:focus-within{box-shadow:0 0 0 .2rem #448aff1a}.md-typeset .note>.admonition-title,.md-typeset .note>summary{background-color:#448aff1a}.md-typeset .note>.admonition-title:before,.md-typeset .note>summary:before{background-color:#448aff;-webkit-mask-image:var(--md-admonition-icon--note);mask-image:var(--md-admonition-icon--note)}.md-typeset .note>.admonition-title:after,.md-typeset .note>summary:after{color:#448aff}.md-typeset .admonition.abstract,.md-typeset details.abstract{border-color:#00b0ff}.md-typeset .admonition.abstract:focus-within,.md-typeset details.abstract:focus-within{box-shadow:0 0 0 .2rem #00b0ff1a}.md-typeset .abstract>.admonition-title,.md-typeset .abstract>summary{background-color:#00b0ff1a}.md-typeset .abstract>.admonition-title:before,.md-typeset .abstract>summary:before{background-color:#00b0ff;-webkit-mask-image:var(--md-admonition-icon--abstract);mask-image:var(--md-admonition-icon--abstract)}.md-typeset .abstract>.admonition-title:after,.md-typeset .abstract>summary:after{color:#00b0ff}.md-typeset .admonition.info,.md-typeset details.info{border-color:#00b8d4}.md-typeset .admonition.info:focus-within,.md-typeset details.info:focus-within{box-shadow:0 0 0 .2rem #00b8d41a}.md-typeset .info>.admonition-title,.md-typeset .info>summary{background-color:#00b8d41a}.md-typeset .info>.admonition-title:before,.md-typeset .info>summary:before{background-color:#00b8d4;-webkit-mask-image:var(--md-admonition-icon--info);mask-image:var(--md-admonition-icon--info)}.md-typeset .info>.admonition-title:after,.md-typeset .info>summary:after{color:#00b8d4}.md-typeset .admonition.tip,.md-typeset details.tip{border-color:#00bfa5}.md-typeset .admonition.tip:focus-within,.md-typeset details.tip:focus-within{box-shadow:0 0 0 .2rem #00bfa51a}.md-typeset .tip>.admonition-title,.md-typeset .tip>summary{background-color:#00bfa51a}.md-typeset .tip>.admonition-title:before,.md-typeset .tip>summary:before{background-color:#00bfa5;-webkit-mask-image:var(--md-admonition-icon--tip);mask-image:var(--md-admonition-icon--tip)}.md-typeset .tip>.admonition-title:after,.md-typeset .tip>summary:after{color:#00bfa5}.md-typeset .admonition.success,.md-typeset details.success{border-color:#00c853}.md-typeset .admonition.success:focus-within,.md-typeset details.success:focus-within{box-shadow:0 0 0 .2rem #00c8531a}.md-typeset .success>.admonition-title,.md-typeset .success>summary{background-color:#00c8531a}.md-typeset .success>.admonition-title:before,.md-typeset .success>summary:before{background-color:#00c853;-webkit-mask-image:var(--md-admonition-icon--success);mask-image:var(--md-admonition-icon--success)}.md-typeset .success>.admonition-title:after,.md-typeset .success>summary:after{color:#00c853}.md-typeset .admonition.question,.md-typeset details.question{border-color:#64dd17}.md-typeset .admonition.question:focus-within,.md-typeset details.question:focus-within{box-shadow:0 0 0 .2rem #64dd171a}.md-typeset .question>.admonition-title,.md-typeset .question>summary{background-color:#64dd171a}.md-typeset .question>.admonition-title:before,.md-typeset .question>summary:before{background-color:#64dd17;-webkit-mask-image:var(--md-admonition-icon--question);mask-image:var(--md-admonition-icon--question)}.md-typeset .question>.admonition-title:after,.md-typeset .question>summary:after{color:#64dd17}.md-typeset .admonition.warning,.md-typeset details.warning{border-color:#ff9100}.md-typeset .admonition.warning:focus-within,.md-typeset details.warning:focus-within{box-shadow:0 0 0 .2rem #ff91001a}.md-typeset .warning>.admonition-title,.md-typeset .warning>summary{background-color:#ff91001a}.md-typeset .warning>.admonition-title:before,.md-typeset .warning>summary:before{background-color:#ff9100;-webkit-mask-image:var(--md-admonition-icon--warning);mask-image:var(--md-admonition-icon--warning)}.md-typeset .warning>.admonition-title:after,.md-typeset .warning>summary:after{color:#ff9100}.md-typeset .admonition.failure,.md-typeset details.failure{border-color:#ff5252}.md-typeset .admonition.failure:focus-within,.md-typeset details.failure:focus-within{box-shadow:0 0 0 .2rem #ff52521a}.md-typeset .failure>.admonition-title,.md-typeset .failure>summary{background-color:#ff52521a}.md-typeset .failure>.admonition-title:before,.md-typeset .failure>summary:before{background-color:#ff5252;-webkit-mask-image:var(--md-admonition-icon--failure);mask-image:var(--md-admonition-icon--failure)}.md-typeset .failure>.admonition-title:after,.md-typeset .failure>summary:after{color:#ff5252}.md-typeset .admonition.danger,.md-typeset details.danger{border-color:#ff1744}.md-typeset .admonition.danger:focus-within,.md-typeset details.danger:focus-within{box-shadow:0 0 0 .2rem #ff17441a}.md-typeset .danger>.admonition-title,.md-typeset .danger>summary{background-color:#ff17441a}.md-typeset .danger>.admonition-title:before,.md-typeset .danger>summary:before{background-color:#ff1744;-webkit-mask-image:var(--md-admonition-icon--danger);mask-image:var(--md-admonition-icon--danger)}.md-typeset .danger>.admonition-title:after,.md-typeset .danger>summary:after{color:#ff1744}.md-typeset .admonition.bug,.md-typeset details.bug{border-color:#f50057}.md-typeset .admonition.bug:focus-within,.md-typeset details.bug:focus-within{box-shadow:0 0 0 .2rem #f500571a}.md-typeset .bug>.admonition-title,.md-typeset .bug>summary{background-color:#f500571a}.md-typeset .bug>.admonition-title:before,.md-typeset .bug>summary:before{background-color:#f50057;-webkit-mask-image:var(--md-admonition-icon--bug);mask-image:var(--md-admonition-icon--bug)}.md-typeset .bug>.admonition-title:after,.md-typeset .bug>summary:after{color:#f50057}.md-typeset .admonition.example,.md-typeset details.example{border-color:#7c4dff}.md-typeset .admonition.example:focus-within,.md-typeset details.example:focus-within{box-shadow:0 0 0 .2rem #7c4dff1a}.md-typeset .example>.admonition-title,.md-typeset .example>summary{background-color:#7c4dff1a}.md-typeset .example>.admonition-title:before,.md-typeset .example>summary:before{background-color:#7c4dff;-webkit-mask-image:var(--md-admonition-icon--example);mask-image:var(--md-admonition-icon--example)}.md-typeset .example>.admonition-title:after,.md-typeset .example>summary:after{color:#7c4dff}.md-typeset .admonition.quote,.md-typeset details.quote{border-color:#9e9e9e}.md-typeset .admonition.quote:focus-within,.md-typeset details.quote:focus-within{box-shadow:0 0 0 .2rem #9e9e9e1a}.md-typeset .quote>.admonition-title,.md-typeset .quote>summary{background-color:#9e9e9e1a}.md-typeset .quote>.admonition-title:before,.md-typeset .quote>summary:before{background-color:#9e9e9e;-webkit-mask-image:var(--md-admonition-icon--quote);mask-image:var(--md-admonition-icon--quote)}.md-typeset .quote>.admonition-title:after,.md-typeset .quote>summary:after{color:#9e9e9e}:root{--md-footnotes-icon:url('data:image/svg+xml;charset=utf-8, ')}.md-typeset .footnote{color:var(--md-default-fg-color--light);font-size:.64rem}[dir=ltr] .md-typeset .footnote>ol{margin-left:0}[dir=rtl] .md-typeset .footnote>ol{margin-right:0}.md-typeset .footnote>ol>li{transition:color 125ms}.md-typeset .footnote>ol>li:target{color:var(--md-default-fg-color)}.md-typeset .footnote>ol>li:focus-within .footnote-backref{opacity:1;transform:translateX(0);transition:none}.md-typeset .footnote>ol>li:hover .footnote-backref,.md-typeset .footnote>ol>li:target .footnote-backref{opacity:1;transform:translateX(0)}.md-typeset .footnote>ol>li>:first-child{margin-top:0}.md-typeset .footnote-ref{font-size:.75em;font-weight:700}html .md-typeset .footnote-ref{outline-offset:.1rem}.md-typeset [id^="fnref:"]:target>.footnote-ref{outline:auto}.md-typeset .footnote-backref{color:var(--md-typeset-a-color);display:inline-block;font-size:0;opacity:0;transform:translateX(.25rem);transition:color .25s,transform .25s .25s,opacity 125ms .25s;vertical-align:text-bottom}@media print{.md-typeset .footnote-backref{color:var(--md-typeset-a-color);opacity:1;transform:translateX(0)}}[dir=rtl] .md-typeset .footnote-backref{transform:translateX(-.25rem)}.md-typeset .footnote-backref:hover{color:var(--md-accent-fg-color)}.md-typeset .footnote-backref:before{background-color:currentcolor;content:"";display:inline-block;height:.8rem;-webkit-mask-image:var(--md-footnotes-icon);mask-image:var(--md-footnotes-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:.8rem}[dir=rtl] .md-typeset .footnote-backref:before{transform:scaleX(-1)}[dir=ltr] .md-typeset .headerlink{margin-left:.5rem}[dir=rtl] .md-typeset .headerlink{margin-right:.5rem}.md-typeset .headerlink{color:var(--md-default-fg-color--lighter);display:inline-block;opacity:0;transition:color .25s,opacity 125ms}@media print{.md-typeset .headerlink{display:none}}.md-typeset .headerlink:focus,.md-typeset :hover>.headerlink,.md-typeset :target>.headerlink{opacity:1;transition:color .25s,opacity 125ms}.md-typeset .headerlink:focus,.md-typeset .headerlink:hover,.md-typeset :target>.headerlink{color:var(--md-accent-fg-color)}.md-typeset :target{--md-scroll-margin:3.6rem;--md-scroll-offset:0rem;scroll-margin-top:calc(var(--md-scroll-margin) - var(--md-scroll-offset))}@media screen and (min-width:76.25em){.md-header--lifted~.md-container .md-typeset :target{--md-scroll-margin:6rem}}.md-typeset h1:target,.md-typeset h2:target,.md-typeset h3:target{--md-scroll-offset:0.2rem}.md-typeset h4:target{--md-scroll-offset:0.15rem}.doc-contents td code{word-break:normal!important}.doc-md-description,.doc-md-description>p:first-child{display:inline}.md-typeset h5 .doc-object-name{text-transform:none}.doc .md-typeset__table,.doc .md-typeset__table table{display:table!important;width:100%}.doc .md-typeset__table tr{display:table-row}.doc-param-default,.doc-type_param-default{float:right}.doc-heading-parameter,.doc-heading-type_parameter{display:inline}.md-typeset .doc-heading-parameter{font-size:inherit}.doc-heading-parameter .headerlink,.doc-heading-type_parameter .headerlink{margin-left:0!important;margin-right:.2rem}.doc-section-title{font-weight:700}.doc-signature .autorefs{color:inherit;text-decoration-style:dotted}:host,:root,[data-md-color-scheme=default]{--doc-symbol-parameter-fg-color:#829bd1;--doc-symbol-type_parameter-fg-color:#829bd1;--doc-symbol-attribute-fg-color:#953800;--doc-symbol-function-fg-color:#8250df;--doc-symbol-method-fg-color:#8250df;--doc-symbol-class-fg-color:#0550ae;--doc-symbol-type_alias-fg-color:#0550ae;--doc-symbol-module-fg-color:#5cad0f;--doc-symbol-parameter-bg-color:#829bd11a;--doc-symbol-type_parameter-bg-color:#829bd11a;--doc-symbol-attribute-bg-color:#9538001a;--doc-symbol-function-bg-color:#8250df1a;--doc-symbol-method-bg-color:#8250df1a;--doc-symbol-class-bg-color:#0550ae1a;--doc-symbol-type_alias-bg-color:#0550ae1a;--doc-symbol-module-bg-color:#5cad0f1a}[data-md-color-scheme=slate]{--doc-symbol-parameter-fg-color:#829bd1;--doc-symbol-type_parameter-fg-color:#829bd1;--doc-symbol-attribute-fg-color:#ffa657;--doc-symbol-function-fg-color:#d2a8ff;--doc-symbol-method-fg-color:#d2a8ff;--doc-symbol-class-fg-color:#79c0ff;--doc-symbol-type_alias-fg-color:#79c0ff;--doc-symbol-module-fg-color:#baff79;--doc-symbol-parameter-bg-color:#829bd11a;--doc-symbol-type_parameter-bg-color:#829bd11a;--doc-symbol-attribute-bg-color:#ffa6571a;--doc-symbol-function-bg-color:#d2a8ff1a;--doc-symbol-method-bg-color:#d2a8ff1a;--doc-symbol-class-bg-color:#79c0ff1a;--doc-symbol-type_alias-bg-color:#79c0ff1a;--doc-symbol-module-bg-color:#baff791a}code.doc-symbol{border-radius:.1rem;font-size:.85em;font-weight:700;padding:0 .3em}a code.doc-symbol-parameter,code.doc-symbol-parameter{background-color:var(--doc-symbol-parameter-bg-color);color:var(--doc-symbol-parameter-fg-color)}code.doc-symbol-parameter:after{content:"param"}a code.doc-symbol-type_parameter,code.doc-symbol-type_parameter{background-color:var(--doc-symbol-type_parameter-bg-color);color:var(--doc-symbol-type_parameter-fg-color)}code.doc-symbol-type_parameter:after{content:"type-param"}a code.doc-symbol-attribute,code.doc-symbol-attribute{background-color:var(--doc-symbol-attribute-bg-color);color:var(--doc-symbol-attribute-fg-color)}code.doc-symbol-attribute:after{content:"attr"}a code.doc-symbol-function,code.doc-symbol-function{background-color:var(--doc-symbol-function-bg-color);color:var(--doc-symbol-function-fg-color)}code.doc-symbol-function:after{content:"func"}a code.doc-symbol-method,code.doc-symbol-method{background-color:var(--doc-symbol-method-bg-color);color:var(--doc-symbol-method-fg-color)}code.doc-symbol-method:after{content:"meth"}a code.doc-symbol-class,code.doc-symbol-class{background-color:var(--doc-symbol-class-bg-color);color:var(--doc-symbol-class-fg-color)}code.doc-symbol-class:after{content:"class"}a code.doc-symbol-type_alias,code.doc-symbol-type_alias{background-color:var(--doc-symbol-type_alias-bg-color);color:var(--doc-symbol-type_alias-fg-color)}code.doc-symbol-type_alias:after{content:"type"}a code.doc-symbol-module,code.doc-symbol-module{background-color:var(--doc-symbol-module-bg-color);color:var(--doc-symbol-module-fg-color)}code.doc-symbol-module:after{content:"mod"}:root{--md-admonition-icon--mkdocstrings-source:url('data:image/svg+xml;charset=utf-8, ') }.md-typeset .admonition.mkdocstrings-source,.md-typeset details.mkdocstrings-source{border:none;padding:0}.md-typeset .admonition.mkdocstrings-source:focus-within,.md-typeset details.mkdocstrings-source:focus-within{box-shadow:none}.md-typeset .mkdocstrings-source>.admonition-title,.md-typeset .mkdocstrings-source>summary{background-color:inherit}.md-typeset .mkdocstrings-source>.admonition-title:before,.md-typeset .mkdocstrings-source>summary:before{background-color:var(--md-default-fg-color);-webkit-mask-image:var(--md-admonition-icon--mkdocstrings-source);mask-image:var(--md-admonition-icon--mkdocstrings-source)}.md-typeset div.arithmatex{overflow:auto}@media screen and (max-width:44.984375em){.md-typeset div.arithmatex{margin:0 -.8rem}.md-typeset div.arithmatex>*{width:min-content}}.md-typeset div.arithmatex>*{margin-left:auto!important;margin-right:auto!important;padding:0 .8rem;touch-action:auto}.md-typeset div.arithmatex>* mjx-container{margin:0!important}.md-typeset div.arithmatex mjx-assistive-mml{height:0}.md-typeset del.critic{background-color:var(--md-typeset-del-color)}.md-typeset del.critic,.md-typeset ins.critic{-webkit-box-decoration-break:clone;box-decoration-break:clone}.md-typeset ins.critic{background-color:var(--md-typeset-ins-color)}.md-typeset .critic.comment{-webkit-box-decoration-break:clone;box-decoration-break:clone;color:var(--md-code-hl-comment-color)}.md-typeset .critic.comment:before{content:"/* "}.md-typeset .critic.comment:after{content:" */"}.md-typeset .critic.block{box-shadow:none;display:block;margin:1em 0;overflow:auto;padding-left:.8rem;padding-right:.8rem}.md-typeset .critic.block>:first-child{margin-top:.5em}.md-typeset .critic.block>:last-child{margin-bottom:.5em}:root{--md-details-icon:url('data:image/svg+xml;charset=utf-8, ')}.md-typeset details{display:flow-root;overflow:visible;padding-top:0}.md-typeset details[open]>summary:after{transform:rotate(90deg)}.md-typeset details:not([open]){box-shadow:none;padding-bottom:0}.md-typeset details:not([open])>summary{border-radius:.1rem}[dir=ltr] .md-typeset summary{padding-right:1.8rem}[dir=rtl] .md-typeset summary{padding-left:1.8rem}[dir=ltr] .md-typeset summary{border-top-left-radius:.1rem}[dir=ltr] .md-typeset summary,[dir=rtl] .md-typeset summary{border-top-right-radius:.1rem}[dir=rtl] .md-typeset summary{border-top-left-radius:.1rem}.md-typeset summary{cursor:pointer;display:block;min-height:1rem;overflow:hidden}.md-typeset summary.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-typeset summary:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}[dir=ltr] .md-typeset summary:after{right:.4rem}[dir=rtl] .md-typeset summary:after{left:.4rem}.md-typeset summary:after{background-color:currentcolor;content:"";height:1rem;-webkit-mask-image:var(--md-details-icon);mask-image:var(--md-details-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.625em;transform:rotate(0deg);transition:transform .25s;width:1rem}[dir=rtl] .md-typeset summary:after{transform:rotate(180deg)}.md-typeset summary::marker{display:none}.md-typeset summary::-webkit-details-marker{display:none}.md-typeset .emojione,.md-typeset .gemoji,.md-typeset .twemoji{--md-icon-size:1.125em;display:inline-flex;height:var(--md-icon-size);vertical-align:text-top}.md-typeset .emojione svg,.md-typeset .gemoji svg,.md-typeset .twemoji svg{fill:currentcolor;max-height:100%;width:var(--md-icon-size)}.md-typeset .emojione svg.lucide,.md-typeset .gemoji svg.lucide,.md-typeset .twemoji svg.lucide{fill:#0000;stroke:currentcolor}.md-typeset .lg,.md-typeset .xl,.md-typeset .xxl,.md-typeset .xxxl{vertical-align:text-bottom}.md-typeset .middle{vertical-align:middle}.md-typeset .lg{--md-icon-size:1.5em}.md-typeset .xl{--md-icon-size:2.25em}.md-typeset .xxl{--md-icon-size:3em}.md-typeset .xxxl{--md-icon-size:4em}.highlight .o,.highlight .ow{color:var(--md-code-hl-operator-color)}.highlight .p{color:var(--md-code-hl-punctuation-color)}.highlight .cpf,.highlight .l,.highlight .s,.highlight .s1,.highlight .s2,.highlight .sb,.highlight .sc,.highlight .si,.highlight .ss{color:var(--md-code-hl-string-color)}.highlight .cp,.highlight .se,.highlight .sh,.highlight .sr,.highlight .sx{color:var(--md-code-hl-special-color)}.highlight .il,.highlight .m,.highlight .mb,.highlight .mf,.highlight .mh,.highlight .mi,.highlight .mo{color:var(--md-code-hl-number-color)}.highlight .k,.highlight .kd,.highlight .kn,.highlight .kp,.highlight .kr,.highlight .kt{color:var(--md-code-hl-keyword-color)}.highlight .kc,.highlight .n{color:var(--md-code-hl-name-color)}.highlight .bp,.highlight .nb,.highlight .no{color:var(--md-code-hl-constant-color)}.highlight .nc,.highlight .ne,.highlight .nf,.highlight .nn{color:var(--md-code-hl-function-color)}.highlight .nd,.highlight .ni,.highlight .nl,.highlight .nt{color:var(--md-code-hl-keyword-color)}.highlight .c,.highlight .c1,.highlight .ch,.highlight .cm,.highlight .cs,.highlight .sd{color:var(--md-code-hl-comment-color)}.highlight .na,.highlight .nv,.highlight .vc,.highlight .vg,.highlight .vi{color:var(--md-code-hl-variable-color)}.highlight .ge,.highlight .gh,.highlight .go,.highlight .gp,.highlight .gr,.highlight .gs,.highlight .gt,.highlight .gu{color:var(--md-code-hl-generic-color)}.highlight .gd,.highlight .gi{border-radius:.1rem;margin:0 -.125em;padding:0 .125em}.highlight .gd{background-color:var(--md-typeset-del-color)}.highlight .gi{background-color:var(--md-typeset-ins-color)}.highlight .hll{background-color:var(--md-code-hl-color--light);box-shadow:2px 0 0 0 var(--md-code-hl-color) inset;display:block;margin:0 -1.1764705882em;padding:0 1.1764705882em}.highlight span.filename{background-color:var(--md-code-bg-color);border-bottom:.05rem solid var(--md-default-fg-color--lightest);border-top-left-radius:.1rem;border-top-right-radius:.1rem;display:flow-root;font-size:.85em;font-weight:700;margin-top:1em;padding:.6617647059em 1.1764705882em;position:relative}.highlight span.filename+pre{margin-top:0}.highlight span.filename+pre>code{border-top-left-radius:0;border-top-right-radius:0}.highlight [data-linenos]:before{background-color:var(--md-code-bg-color);box-shadow:-.05rem 0 var(--md-default-fg-color--lightest) inset;color:var(--md-default-fg-color--light);content:attr(data-linenos);float:left;left:-1.1764705882em;margin-left:-1.1764705882em;margin-right:1.1764705882em;padding-left:1.1764705882em;position:sticky;-webkit-user-select:none;user-select:none;z-index:3}.highlight code>span[id^=__span]>:last-child .md-annotation{margin-right:2.4rem}.highlight code[data-md-copying]{display:initial}.highlight code[data-md-copying] .hll{display:contents}.highlight code[data-md-copying] .md-annotation{display:none}.highlighttable{display:flow-root}.highlighttable tbody,.highlighttable td{display:block;padding:0}.highlighttable tr{display:flex}.highlighttable pre{margin:0}.highlighttable th.filename{flex-grow:1;padding:0;text-align:left}.highlighttable th.filename span.filename{margin-top:0}.highlighttable .linenos{background-color:var(--md-code-bg-color);border-bottom-left-radius:.1rem;border-top-left-radius:.1rem;font-size:.85em;padding:.7720588235em 0 .7720588235em 1.1764705882em;-webkit-user-select:none;user-select:none}.highlighttable .linenodiv{box-shadow:-.05rem 0 var(--md-default-fg-color--lightest) inset}.highlighttable .linenodiv pre{color:var(--md-default-fg-color--light);text-align:right}.highlighttable .linenodiv span[class]{padding-right:.5882352941em}.highlighttable .code{flex:1;min-width:0}.linenodiv a{color:inherit}.md-typeset .highlighttable{direction:ltr;margin:1em 0}.md-typeset .highlighttable>tbody>tr>.code>div>pre>code{border-bottom-left-radius:0;border-top-left-radius:0}.md-typeset .highlight+.result{border:.05rem solid var(--md-code-bg-color);border-bottom-left-radius:.1rem;border-bottom-right-radius:.1rem;border-top-width:.1rem;margin-top:-1.125em;overflow:visible;padding:0 1em}.md-typeset .highlight+.result:after{clear:both;content:"";display:block}@media screen and (max-width:44.984375em){.md-content__inner>.highlight{margin:1em -.8rem}.md-content__inner>.highlight>.filename,.md-content__inner>.highlight>.highlighttable>tbody>tr>.code>div>pre>code,.md-content__inner>.highlight>.highlighttable>tbody>tr>.filename span.filename,.md-content__inner>.highlight>.highlighttable>tbody>tr>.linenos,.md-content__inner>.highlight>pre>code{border-radius:0}.md-content__inner>.highlight+.result{border-left-width:0;border-radius:0;border-right-width:0;margin-left:-.8rem;margin-right:-.8rem}}.md-typeset .keys kbd:after,.md-typeset .keys kbd:before{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;color:inherit;margin:0;position:relative}.md-typeset .keys span{color:var(--md-default-fg-color--light);padding:0 .2em}.md-typeset .keys .key-alt:before,.md-typeset .keys .key-left-alt:before,.md-typeset .keys .key-right-alt:before{content:"⎇";padding-right:.4em}.md-typeset .keys .key-command:before,.md-typeset .keys .key-left-command:before,.md-typeset .keys .key-right-command:before{content:"⌘";padding-right:.4em}.md-typeset .keys .key-control:before,.md-typeset .keys .key-left-control:before,.md-typeset .keys .key-right-control:before{content:"⌃";padding-right:.4em}.md-typeset .keys .key-left-meta:before,.md-typeset .keys .key-meta:before,.md-typeset .keys .key-right-meta:before{content:"◆";padding-right:.4em}.md-typeset .keys .key-left-option:before,.md-typeset .keys .key-option:before,.md-typeset .keys .key-right-option:before{content:"⌥";padding-right:.4em}.md-typeset .keys .key-left-shift:before,.md-typeset .keys .key-right-shift:before,.md-typeset .keys .key-shift:before{content:"⇧";padding-right:.4em}.md-typeset .keys .key-left-super:before,.md-typeset .keys .key-right-super:before,.md-typeset .keys .key-super:before{content:"❖";padding-right:.4em}.md-typeset .keys .key-left-windows:before,.md-typeset .keys .key-right-windows:before,.md-typeset .keys .key-windows:before{content:"⊞";padding-right:.4em}.md-typeset .keys .key-arrow-down:before{content:"↓";padding-right:.4em}.md-typeset .keys .key-arrow-left:before{content:"←";padding-right:.4em}.md-typeset .keys .key-arrow-right:before{content:"→";padding-right:.4em}.md-typeset .keys .key-arrow-up:before{content:"↑";padding-right:.4em}.md-typeset .keys .key-backspace:before{content:"⌫";padding-right:.4em}.md-typeset .keys .key-backtab:before{content:"⇤";padding-right:.4em}.md-typeset .keys .key-caps-lock:before{content:"⇪";padding-right:.4em}.md-typeset .keys .key-clear:before{content:"⌧";padding-right:.4em}.md-typeset .keys .key-context-menu:before{content:"☰";padding-right:.4em}.md-typeset .keys .key-delete:before{content:"⌦";padding-right:.4em}.md-typeset .keys .key-eject:before{content:"⏏";padding-right:.4em}.md-typeset .keys .key-end:before{content:"⤓";padding-right:.4em}.md-typeset .keys .key-escape:before{content:"⎋";padding-right:.4em}.md-typeset .keys .key-home:before{content:"⤒";padding-right:.4em}.md-typeset .keys .key-insert:before{content:"⎀";padding-right:.4em}.md-typeset .keys .key-page-down:before{content:"⇟";padding-right:.4em}.md-typeset .keys .key-page-up:before{content:"⇞";padding-right:.4em}.md-typeset .keys .key-print-screen:before{content:"⎙";padding-right:.4em}.md-typeset .keys .key-tab:after{content:"⇥";padding-left:.4em}.md-typeset .keys .key-num-enter:after{content:"⌤";padding-left:.4em}.md-typeset .keys .key-enter:after{content:"⏎";padding-left:.4em}:root{--md-tabbed-icon--prev:url('data:image/svg+xml;charset=utf-8, ');--md-tabbed-icon--next:url('data:image/svg+xml;charset=utf-8, ')}.md-typeset .tabbed-set{border-radius:.1rem;display:flex;flex-flow:column wrap;margin:1em 0;position:relative}.md-typeset .tabbed-set>input{height:0;opacity:0;position:absolute;width:0}.md-typeset .tabbed-set>input:target{--md-scroll-offset:0.625em}.md-typeset .tabbed-set>input.focus-visible~.tabbed-labels:before{background-color:var(--md-accent-fg-color)}.md-typeset .tabbed-labels{-ms-overflow-style:none;box-shadow:0 -.05rem var(--md-default-fg-color--lightest) inset;display:flex;max-width:100%;overflow:auto;scrollbar-width:none}@media print{.md-typeset .tabbed-labels{display:contents}}@media screen{.js .md-typeset .tabbed-labels{position:relative}.js .md-typeset .tabbed-labels:before{background:var(--md-default-fg-color);bottom:0;content:"";display:block;height:2px;left:0;position:absolute;transform:translateX(var(--md-indicator-x));transition:width 225ms,background-color .25s,transform .25s;transition-timing-function:cubic-bezier(.4,0,.2,1);width:var(--md-indicator-width)}}.md-typeset .tabbed-labels::-webkit-scrollbar{display:none}.md-typeset .tabbed-labels>label{border-bottom:.1rem solid #0000;border-radius:.1rem .1rem 0 0;color:var(--md-default-fg-color--light);cursor:pointer;flex-shrink:0;font-size:.64rem;font-weight:700;padding:.78125em 1.25em .625em;scroll-margin-inline-start:1rem;transition:background-color .25s,color .25s;white-space:nowrap;width:auto}@media print{.md-typeset .tabbed-labels>label:first-child{order:1}.md-typeset .tabbed-labels>label:nth-child(2){order:2}.md-typeset .tabbed-labels>label:nth-child(3){order:3}.md-typeset .tabbed-labels>label:nth-child(4){order:4}.md-typeset .tabbed-labels>label:nth-child(5){order:5}.md-typeset .tabbed-labels>label:nth-child(6){order:6}.md-typeset .tabbed-labels>label:nth-child(7){order:7}.md-typeset .tabbed-labels>label:nth-child(8){order:8}.md-typeset .tabbed-labels>label:nth-child(9){order:9}.md-typeset .tabbed-labels>label:nth-child(10){order:10}.md-typeset .tabbed-labels>label:nth-child(11){order:11}.md-typeset .tabbed-labels>label:nth-child(12){order:12}.md-typeset .tabbed-labels>label:nth-child(13){order:13}.md-typeset .tabbed-labels>label:nth-child(14){order:14}.md-typeset .tabbed-labels>label:nth-child(15){order:15}.md-typeset .tabbed-labels>label:nth-child(16){order:16}.md-typeset .tabbed-labels>label:nth-child(17){order:17}.md-typeset .tabbed-labels>label:nth-child(18){order:18}.md-typeset .tabbed-labels>label:nth-child(19){order:19}.md-typeset .tabbed-labels>label:nth-child(20){order:20}}.md-typeset .tabbed-labels>label:hover{color:var(--md-default-fg-color)}.md-typeset .tabbed-labels>label>[href]:first-child{color:inherit}.md-typeset .tabbed-labels--linked>label{padding:0}.md-typeset .tabbed-labels--linked>label>a{display:block;padding:.78125em 1.25em .625em}.md-typeset .tabbed-content{width:100%}@media print{.md-typeset .tabbed-content{display:contents}}.md-typeset .tabbed-block{display:none}@media print{.md-typeset .tabbed-block{display:block}.md-typeset .tabbed-block:first-child{order:1}.md-typeset .tabbed-block:nth-child(2){order:2}.md-typeset .tabbed-block:nth-child(3){order:3}.md-typeset .tabbed-block:nth-child(4){order:4}.md-typeset .tabbed-block:nth-child(5){order:5}.md-typeset .tabbed-block:nth-child(6){order:6}.md-typeset .tabbed-block:nth-child(7){order:7}.md-typeset .tabbed-block:nth-child(8){order:8}.md-typeset .tabbed-block:nth-child(9){order:9}.md-typeset .tabbed-block:nth-child(10){order:10}.md-typeset .tabbed-block:nth-child(11){order:11}.md-typeset .tabbed-block:nth-child(12){order:12}.md-typeset .tabbed-block:nth-child(13){order:13}.md-typeset .tabbed-block:nth-child(14){order:14}.md-typeset .tabbed-block:nth-child(15){order:15}.md-typeset .tabbed-block:nth-child(16){order:16}.md-typeset .tabbed-block:nth-child(17){order:17}.md-typeset .tabbed-block:nth-child(18){order:18}.md-typeset .tabbed-block:nth-child(19){order:19}.md-typeset .tabbed-block:nth-child(20){order:20}}.md-typeset .tabbed-block>.highlight:first-child>pre,.md-typeset .tabbed-block>pre:first-child{margin:0}.md-typeset .tabbed-block>.highlight:first-child>pre>code,.md-typeset .tabbed-block>pre:first-child>code{border-top-left-radius:0;border-top-right-radius:0}.md-typeset .tabbed-block>.highlight:first-child>.filename{border-top-left-radius:0;border-top-right-radius:0;margin:0}.md-typeset .tabbed-block>.highlight:first-child>.highlighttable{margin:0}.md-typeset .tabbed-block>.highlight:first-child>.highlighttable>tbody>tr>.filename span.filename,.md-typeset .tabbed-block>.highlight:first-child>.highlighttable>tbody>tr>.linenos{border-top-left-radius:0;border-top-right-radius:0;margin:0}.md-typeset .tabbed-block>.highlight:first-child>.highlighttable>tbody>tr>.code>div>pre>code{border-top-left-radius:0;border-top-right-radius:0}.md-typeset .tabbed-block>.highlight:first-child+.result{margin-top:-.125em}.md-typeset .tabbed-block>.tabbed-set{margin:0}.md-typeset .tabbed-button{align-self:center;border-radius:100%;color:var(--md-default-fg-color--light);cursor:pointer;display:block;height:.9rem;margin-top:.1rem;pointer-events:auto;transition:background-color .25s;width:.9rem}.md-typeset .tabbed-button:hover{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-typeset .tabbed-button:after{background-color:currentcolor;content:"";display:block;height:100%;-webkit-mask-image:var(--md-tabbed-icon--prev);mask-image:var(--md-tabbed-icon--prev);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:background-color .25s,transform .25s;width:100%}.md-typeset .tabbed-control{background:linear-gradient(to right,var(--md-default-bg-color) 60%,#0000);display:flex;height:1.9rem;justify-content:start;pointer-events:none;position:absolute;transition:opacity 125ms;width:1.2rem}[dir=rtl] .md-typeset .tabbed-control{transform:rotate(180deg)}.md-typeset .tabbed-control[hidden]{opacity:0}.md-typeset .tabbed-control--next{background:linear-gradient(to left,var(--md-default-bg-color) 60%,#0000);justify-content:end;right:0}.md-typeset .tabbed-control--next .tabbed-button:after{-webkit-mask-image:var(--md-tabbed-icon--next);mask-image:var(--md-tabbed-icon--next)}@media screen and (max-width:44.984375em){[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels{padding-left:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels{padding-right:.8rem}.md-content__inner>.tabbed-set .tabbed-labels{margin:0 -.8rem;max-width:100vw;scroll-padding-inline-start:.8rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels:after{padding-right:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels:after{padding-left:.8rem}.md-content__inner>.tabbed-set .tabbed-labels:after{content:""}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{padding-left:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{padding-right:.8rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{margin-left:-.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{margin-right:-.8rem}.md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{width:2rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{padding-right:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{padding-left:.8rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{margin-right:-.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{margin-left:-.8rem}.md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{width:2rem}}@media screen{.md-typeset .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9){color:var(--md-default-fg-color)}.md-typeset .no-js .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.md-typeset .no-js .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.md-typeset .no-js .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.md-typeset .no-js .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.md-typeset .no-js .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.md-typeset .no-js .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.md-typeset .no-js .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.md-typeset .no-js .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.md-typeset .no-js .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.md-typeset .no-js .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.md-typeset .no-js .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.md-typeset .no-js .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.md-typeset .no-js .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.md-typeset .no-js .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.md-typeset .no-js .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.md-typeset .no-js .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.md-typeset .no-js .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.md-typeset .no-js .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.md-typeset .no-js .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.md-typeset .no-js .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9),.md-typeset [role=dialog] .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.md-typeset [role=dialog] .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.md-typeset [role=dialog] .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.md-typeset [role=dialog] .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.md-typeset [role=dialog] .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.md-typeset [role=dialog] .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.md-typeset [role=dialog] .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.md-typeset [role=dialog] .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.md-typeset [role=dialog] .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.md-typeset [role=dialog] .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.md-typeset [role=dialog] .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.md-typeset [role=dialog] .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.md-typeset [role=dialog] .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.md-typeset [role=dialog] .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.md-typeset [role=dialog] .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.md-typeset [role=dialog] .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.md-typeset [role=dialog] .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.md-typeset [role=dialog] .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.md-typeset [role=dialog] .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.md-typeset [role=dialog] .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9),.no-js .md-typeset .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.no-js .md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.no-js .md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.no-js .md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.no-js .md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.no-js .md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.no-js .md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.no-js .md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.no-js .md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.no-js .md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.no-js .md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.no-js .md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.no-js .md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.no-js .md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.no-js .md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.no-js .md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.no-js .md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.no-js .md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.no-js .md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.no-js .md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9),[role=dialog] .md-typeset .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,[role=dialog] .md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),[role=dialog] .md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),[role=dialog] .md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),[role=dialog] .md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),[role=dialog] .md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),[role=dialog] .md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),[role=dialog] .md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),[role=dialog] .md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),[role=dialog] .md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),[role=dialog] .md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),[role=dialog] .md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),[role=dialog] .md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),[role=dialog] .md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),[role=dialog] .md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),[role=dialog] .md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),[role=dialog] .md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),[role=dialog] .md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),[role=dialog] .md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),[role=dialog] .md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9){border-color:var(--md-default-fg-color)}}.md-typeset .tabbed-set>input:first-child.focus-visible~.tabbed-labels>:first-child,.md-typeset .tabbed-set>input:nth-child(10).focus-visible~.tabbed-labels>:nth-child(10),.md-typeset .tabbed-set>input:nth-child(11).focus-visible~.tabbed-labels>:nth-child(11),.md-typeset .tabbed-set>input:nth-child(12).focus-visible~.tabbed-labels>:nth-child(12),.md-typeset .tabbed-set>input:nth-child(13).focus-visible~.tabbed-labels>:nth-child(13),.md-typeset .tabbed-set>input:nth-child(14).focus-visible~.tabbed-labels>:nth-child(14),.md-typeset .tabbed-set>input:nth-child(15).focus-visible~.tabbed-labels>:nth-child(15),.md-typeset .tabbed-set>input:nth-child(16).focus-visible~.tabbed-labels>:nth-child(16),.md-typeset .tabbed-set>input:nth-child(17).focus-visible~.tabbed-labels>:nth-child(17),.md-typeset .tabbed-set>input:nth-child(18).focus-visible~.tabbed-labels>:nth-child(18),.md-typeset .tabbed-set>input:nth-child(19).focus-visible~.tabbed-labels>:nth-child(19),.md-typeset .tabbed-set>input:nth-child(2).focus-visible~.tabbed-labels>:nth-child(2),.md-typeset .tabbed-set>input:nth-child(20).focus-visible~.tabbed-labels>:nth-child(20),.md-typeset .tabbed-set>input:nth-child(3).focus-visible~.tabbed-labels>:nth-child(3),.md-typeset .tabbed-set>input:nth-child(4).focus-visible~.tabbed-labels>:nth-child(4),.md-typeset .tabbed-set>input:nth-child(5).focus-visible~.tabbed-labels>:nth-child(5),.md-typeset .tabbed-set>input:nth-child(6).focus-visible~.tabbed-labels>:nth-child(6),.md-typeset .tabbed-set>input:nth-child(7).focus-visible~.tabbed-labels>:nth-child(7),.md-typeset .tabbed-set>input:nth-child(8).focus-visible~.tabbed-labels>:nth-child(8),.md-typeset .tabbed-set>input:nth-child(9).focus-visible~.tabbed-labels>:nth-child(9){color:var(--md-accent-fg-color)}.md-typeset .tabbed-set>input:first-child:checked~.tabbed-content>:first-child,.md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-content>:nth-child(10),.md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-content>:nth-child(11),.md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-content>:nth-child(12),.md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-content>:nth-child(13),.md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-content>:nth-child(14),.md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-content>:nth-child(15),.md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-content>:nth-child(16),.md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-content>:nth-child(17),.md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-content>:nth-child(18),.md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-content>:nth-child(19),.md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-content>:nth-child(2),.md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-content>:nth-child(20),.md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-content>:nth-child(3),.md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-content>:nth-child(4),.md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-content>:nth-child(5),.md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-content>:nth-child(6),.md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-content>:nth-child(7),.md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-content>:nth-child(8),.md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-content>:nth-child(9){display:block}:root{--md-tasklist-icon:url('data:image/svg+xml;charset=utf-8, ');--md-tasklist-icon--checked:url('data:image/svg+xml;charset=utf-8, ')}.md-typeset .task-list-item{list-style-type:none;position:relative}[dir=ltr] .md-typeset .task-list-item [type=checkbox]{left:-2em}[dir=rtl] .md-typeset .task-list-item [type=checkbox]{right:-2em}.md-typeset .task-list-item [type=checkbox]{position:absolute;top:.45em}.md-typeset .task-list-control [type=checkbox]{opacity:0;z-index:-1}[dir=ltr] .md-typeset .task-list-indicator:before{left:-1.5em}[dir=rtl] .md-typeset .task-list-indicator:before{right:-1.5em}.md-typeset .task-list-indicator:before{background-color:var(--md-default-fg-color--lightest);content:"";height:1.25em;-webkit-mask-image:var(--md-tasklist-icon);mask-image:var(--md-tasklist-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.15em;width:1.25em}.md-typeset [type=checkbox]:checked+.task-list-indicator:before{background-color:#00e676;-webkit-mask-image:var(--md-tasklist-icon--checked);mask-image:var(--md-tasklist-icon--checked)}@media print{.giscus,[id=__comments]{display:none}}:root>*{--md-mermaid-font-family:var(--md-text-font-family),sans-serif;--md-mermaid-edge-color:var(--md-code-fg-color);--md-mermaid-node-bg-color:var(--md-accent-fg-color--transparent);--md-mermaid-node-fg-color:var(--md-accent-fg-color);--md-mermaid-label-bg-color:var(--md-default-bg-color);--md-mermaid-label-fg-color:var(--md-code-fg-color);--md-mermaid-sequence-actor-bg-color:var(--md-mermaid-label-bg-color);--md-mermaid-sequence-actor-fg-color:var(--md-mermaid-label-fg-color);--md-mermaid-sequence-actor-border-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-actor-line-color:var(--md-default-fg-color--lighter);--md-mermaid-sequence-actorman-bg-color:var(--md-mermaid-label-bg-color);--md-mermaid-sequence-actorman-line-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-box-bg-color:var(--md-mermaid-node-bg-color);--md-mermaid-sequence-box-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-label-bg-color:var(--md-mermaid-node-bg-color);--md-mermaid-sequence-label-fg-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-loop-bg-color:var(--md-mermaid-node-bg-color);--md-mermaid-sequence-loop-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-loop-border-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-message-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-message-line-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-note-bg-color:var(--md-mermaid-label-bg-color);--md-mermaid-sequence-note-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-note-border-color:var(--md-mermaid-label-fg-color);--md-mermaid-sequence-number-bg-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-number-fg-color:var(--md-accent-bg-color)}.mermaid{line-height:normal;margin:1em 0}.md-typeset .grid{grid-gap:.4rem;display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,16rem),1fr));margin:1em 0}.md-typeset .grid.cards>ol,.md-typeset .grid.cards>ul{display:contents}.md-typeset .grid.cards>ol>li,.md-typeset .grid.cards>ul>li,.md-typeset .grid>.card{border:.05rem solid var(--md-default-fg-color--lightest);border-radius:.1rem;display:block;margin:0;padding:.8rem;transition:border .25s,box-shadow .25s}.md-typeset .grid.cards>ol>li:focus-within,.md-typeset .grid.cards>ol>li:hover,.md-typeset .grid.cards>ul>li:focus-within,.md-typeset .grid.cards>ul>li:hover,.md-typeset .grid>.card:focus-within,.md-typeset .grid>.card:hover{border-color:#0000;box-shadow:var(--md-shadow-z2)}.md-typeset .grid.cards>ol>li>hr,.md-typeset .grid.cards>ul>li>hr,.md-typeset .grid>.card>hr{margin-bottom:1em;margin-top:1em}.md-typeset .grid.cards>ol>li>:first-child,.md-typeset .grid.cards>ul>li>:first-child,.md-typeset .grid>.card>:first-child{margin-top:0}.md-typeset .grid.cards>ol>li>:last-child,.md-typeset .grid.cards>ul>li>:last-child,.md-typeset .grid>.card>:last-child{margin-bottom:0}.md-typeset .grid>*,.md-typeset .grid>.admonition,.md-typeset .grid>.highlight>*,.md-typeset .grid>.highlighttable,.md-typeset .grid>.md-typeset details,.md-typeset .grid>details,.md-typeset .grid>pre{margin-bottom:0;margin-top:0}.md-typeset .grid>.highlight>pre:only-child,.md-typeset .grid>.highlight>pre>code,.md-typeset .grid>.highlighttable,.md-typeset .grid>.highlighttable>tbody,.md-typeset .grid>.highlighttable>tbody>tr,.md-typeset .grid>.highlighttable>tbody>tr>.code,.md-typeset .grid>.highlighttable>tbody>tr>.code>.highlight,.md-typeset .grid>.highlighttable>tbody>tr>.code>.highlight>pre,.md-typeset .grid>.highlighttable>tbody>tr>.code>.highlight>pre>code{height:100%}.md-typeset .grid>.tabbed-set{margin-bottom:0;margin-top:0}@media screen and (min-width:45em){[dir=ltr] .md-typeset .inline{float:left}[dir=rtl] .md-typeset .inline{float:right}[dir=ltr] .md-typeset .inline{margin-right:.8rem}[dir=rtl] .md-typeset .inline{margin-left:.8rem}.md-typeset .inline{margin-bottom:.8rem;margin-top:0;width:11.7rem}[dir=ltr] .md-typeset .inline.end{float:right}[dir=rtl] .md-typeset .inline.end{float:left}[dir=ltr] .md-typeset .inline.end{margin-left:.8rem;margin-right:0}[dir=rtl] .md-typeset .inline.end{margin-left:0;margin-right:.8rem}}
\ No newline at end of file
diff --git a/v2.2/assets/stylesheets/classic/palette.7dc9a0ad.min.css b/v2.2/assets/stylesheets/classic/palette.7dc9a0ad.min.css
new file mode 100644
index 0000000..2d83819
--- /dev/null
+++ b/v2.2/assets/stylesheets/classic/palette.7dc9a0ad.min.css
@@ -0,0 +1 @@
+@media screen{[data-md-color-scheme=slate]{--md-default-fg-color:hsla(var(--md-hue),15%,90%,0.82);--md-default-fg-color--light:hsla(var(--md-hue),15%,90%,0.56);--md-default-fg-color--lighter:hsla(var(--md-hue),15%,90%,0.32);--md-default-fg-color--lightest:hsla(var(--md-hue),15%,90%,0.12);--md-default-bg-color:hsla(var(--md-hue),15%,14%,1);--md-default-bg-color--light:hsla(var(--md-hue),15%,14%,0.54);--md-default-bg-color--lighter:hsla(var(--md-hue),15%,14%,0.26);--md-default-bg-color--lightest:hsla(var(--md-hue),15%,14%,0.07);--md-code-fg-color:hsla(var(--md-hue),18%,86%,0.82);--md-code-bg-color:hsla(var(--md-hue),15%,18%,1);--md-code-bg-color--light:hsla(var(--md-hue),15%,18%,0.9);--md-code-bg-color--lighter:hsla(var(--md-hue),15%,18%,0.54);--md-code-hl-color:#2977ff;--md-code-hl-color--light:#2977ff1a;--md-code-hl-number-color:#e6695b;--md-code-hl-special-color:#f06090;--md-code-hl-function-color:#c973d9;--md-code-hl-constant-color:#9383e2;--md-code-hl-keyword-color:#6791e0;--md-code-hl-string-color:#2fb170;--md-code-hl-name-color:var(--md-code-fg-color);--md-code-hl-operator-color:var(--md-default-fg-color--light);--md-code-hl-punctuation-color:var(--md-default-fg-color--light);--md-code-hl-comment-color:var(--md-default-fg-color--light);--md-code-hl-generic-color:var(--md-default-fg-color--light);--md-code-hl-variable-color:var(--md-default-fg-color--light);--md-typeset-color:var(--md-default-fg-color);--md-typeset-a-color:var(--md-primary-fg-color);--md-typeset-kbd-color:hsla(var(--md-hue),15%,90%,0.12);--md-typeset-kbd-accent-color:hsla(var(--md-hue),15%,90%,0.2);--md-typeset-kbd-border-color:hsla(var(--md-hue),15%,14%,1);--md-typeset-mark-color:#4287ff4d;--md-typeset-table-color:hsla(var(--md-hue),15%,95%,0.12);--md-typeset-table-color--light:hsla(var(--md-hue),15%,95%,0.035);--md-admonition-fg-color:var(--md-default-fg-color);--md-admonition-bg-color:var(--md-default-bg-color);--md-footer-bg-color:hsla(var(--md-hue),15%,10%,0.87);--md-footer-bg-color--dark:hsla(var(--md-hue),15%,8%,1);--md-shadow-z1:0 0.2rem 0.5rem #0000000d,0 0 0.05rem #0000001a;--md-shadow-z2:0 0.2rem 0.5rem #00000040,0 0 0.05rem #00000040;--md-shadow-z3:0 0.2rem 0.5rem #0006,0 0 0.05rem #00000059;color-scheme:dark}[data-md-color-scheme=slate] img[src$="#gh-light-mode-only"],[data-md-color-scheme=slate] img[src$="#only-light"]{display:none}[data-md-color-scheme=slate]{--color-foreground:255 255 255;--color-background:22 23 26;--color-background-subtle:33 34 38;--color-backdrop:11 12 15}[data-md-color-scheme=slate][data-md-color-primary=pink]{--md-typeset-a-color:#ed5487}[data-md-color-scheme=slate][data-md-color-primary=purple]{--md-typeset-a-color:#c46fd3}[data-md-color-scheme=slate][data-md-color-primary=deep-purple]{--md-typeset-a-color:#a47bea}[data-md-color-scheme=slate][data-md-color-primary=indigo]{--md-typeset-a-color:#5488e8}[data-md-color-scheme=slate][data-md-color-primary=teal]{--md-typeset-a-color:#00ccb8}[data-md-color-scheme=slate][data-md-color-primary=green]{--md-typeset-a-color:#71c174}[data-md-color-scheme=slate][data-md-color-primary=deep-orange]{--md-typeset-a-color:#ff764d}[data-md-color-scheme=slate][data-md-color-primary=brown]{--md-typeset-a-color:#c1775c}[data-md-color-scheme=slate][data-md-color-primary=black],[data-md-color-scheme=slate][data-md-color-primary=blue-grey],[data-md-color-scheme=slate][data-md-color-primary=grey],[data-md-color-scheme=slate][data-md-color-primary=white]{--md-typeset-a-color:#5e8bde}[data-md-color-switching] *,[data-md-color-switching] :after,[data-md-color-switching] :before{transition-duration:0ms!important}}[data-md-color-accent=red]{--md-accent-fg-color:#ff1947;--md-accent-fg-color--transparent:#ff19471a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=pink]{--md-accent-fg-color:#f50056;--md-accent-fg-color--transparent:#f500561a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=purple]{--md-accent-fg-color:#df41fb;--md-accent-fg-color--transparent:#df41fb1a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=deep-purple]{--md-accent-fg-color:#7c4dff;--md-accent-fg-color--transparent:#7c4dff1a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=indigo]{--md-accent-fg-color:#526cfe;--md-accent-fg-color--transparent:#526cfe1a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=blue]{--md-accent-fg-color:#4287ff;--md-accent-fg-color--transparent:#4287ff1a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=light-blue]{--md-accent-fg-color:#0091eb;--md-accent-fg-color--transparent:#0091eb1a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=cyan]{--md-accent-fg-color:#00bad6;--md-accent-fg-color--transparent:#00bad61a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=teal]{--md-accent-fg-color:#00bda4;--md-accent-fg-color--transparent:#00bda41a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=green]{--md-accent-fg-color:#00c753;--md-accent-fg-color--transparent:#00c7531a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=light-green]{--md-accent-fg-color:#63de17;--md-accent-fg-color--transparent:#63de171a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=lime]{--md-accent-fg-color:#b0eb00;--md-accent-fg-color--transparent:#b0eb001a;--md-accent-bg-color:#000000de;--md-accent-bg-color--light:#0000008a}[data-md-color-accent=yellow]{--md-accent-fg-color:#ffd500;--md-accent-fg-color--transparent:#ffd5001a;--md-accent-bg-color:#000000de;--md-accent-bg-color--light:#0000008a}[data-md-color-accent=amber]{--md-accent-fg-color:#fa0;--md-accent-fg-color--transparent:#ffaa001a;--md-accent-bg-color:#000000de;--md-accent-bg-color--light:#0000008a}[data-md-color-accent=orange]{--md-accent-fg-color:#ff9100;--md-accent-fg-color--transparent:#ff91001a;--md-accent-bg-color:#000000de;--md-accent-bg-color--light:#0000008a}[data-md-color-accent=deep-orange]{--md-accent-fg-color:#ff6e42;--md-accent-fg-color--transparent:#ff6e421a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-primary=red]{--md-primary-fg-color:#ef5552;--md-primary-fg-color--light:#e57171;--md-primary-fg-color--dark:#e53734;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=pink]{--md-primary-fg-color:#e92063;--md-primary-fg-color--light:#ec417a;--md-primary-fg-color--dark:#c3185d;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=purple]{--md-primary-fg-color:#ab47bd;--md-primary-fg-color--light:#bb69c9;--md-primary-fg-color--dark:#8c24a8;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=deep-purple]{--md-primary-fg-color:#7e56c2;--md-primary-fg-color--light:#9574cd;--md-primary-fg-color--dark:#673ab6;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=indigo]{--md-primary-fg-color:#4051b5;--md-primary-fg-color--light:#5d6cc0;--md-primary-fg-color--dark:#303fa1;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=blue]{--md-primary-fg-color:#2094f3;--md-primary-fg-color--light:#42a5f5;--md-primary-fg-color--dark:#1975d2;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=light-blue]{--md-primary-fg-color:#02a6f2;--md-primary-fg-color--light:#28b5f6;--md-primary-fg-color--dark:#0287cf;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=cyan]{--md-primary-fg-color:#00bdd6;--md-primary-fg-color--light:#25c5da;--md-primary-fg-color--dark:#0097a8;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=teal]{--md-primary-fg-color:#009485;--md-primary-fg-color--light:#26a699;--md-primary-fg-color--dark:#007a6c;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=green]{--md-primary-fg-color:#4cae4f;--md-primary-fg-color--light:#68bb6c;--md-primary-fg-color--dark:#398e3d;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=light-green]{--md-primary-fg-color:#8bc34b;--md-primary-fg-color--light:#9ccc66;--md-primary-fg-color--dark:#689f38;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=lime]{--md-primary-fg-color:#cbdc38;--md-primary-fg-color--light:#d3e156;--md-primary-fg-color--dark:#b0b52c;--md-primary-bg-color:#000000de;--md-primary-bg-color--light:#0000008a}[data-md-color-primary=yellow]{--md-primary-fg-color:#ffec3d;--md-primary-fg-color--light:#ffee57;--md-primary-fg-color--dark:#fbc02d;--md-primary-bg-color:#000000de;--md-primary-bg-color--light:#0000008a}[data-md-color-primary=amber]{--md-primary-fg-color:#ffc105;--md-primary-fg-color--light:#ffc929;--md-primary-fg-color--dark:#ffa200;--md-primary-bg-color:#000000de;--md-primary-bg-color--light:#0000008a}[data-md-color-primary=orange]{--md-primary-fg-color:#ffa724;--md-primary-fg-color--light:#ffa724;--md-primary-fg-color--dark:#fa8900;--md-primary-bg-color:#000000de;--md-primary-bg-color--light:#0000008a}[data-md-color-primary=deep-orange]{--md-primary-fg-color:#ff6e42;--md-primary-fg-color--light:#ff8a66;--md-primary-fg-color--dark:#f4511f;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=brown]{--md-primary-fg-color:#795649;--md-primary-fg-color--light:#8d6e62;--md-primary-fg-color--dark:#5d4037;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=grey]{--md-primary-fg-color:#757575;--md-primary-fg-color--light:#9e9e9e;--md-primary-fg-color--dark:#616161;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3;--md-typeset-a-color:#4051b5}[data-md-color-primary=blue-grey]{--md-primary-fg-color:#546d78;--md-primary-fg-color--light:#607c8a;--md-primary-fg-color--dark:#455a63;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3;--md-typeset-a-color:#4051b5}[data-md-color-primary=light-green]:not([data-md-color-scheme=slate]){--md-typeset-a-color:#72ad2e}[data-md-color-primary=lime]:not([data-md-color-scheme=slate]){--md-typeset-a-color:#8b990a}[data-md-color-primary=yellow]:not([data-md-color-scheme=slate]){--md-typeset-a-color:#b8a500}[data-md-color-primary=amber]:not([data-md-color-scheme=slate]){--md-typeset-a-color:#d19d00}[data-md-color-primary=orange]:not([data-md-color-scheme=slate]){--md-typeset-a-color:#e68a00}[data-md-color-primary=white]{--md-primary-fg-color:hsla(var(--md-hue),0%,100%,1);--md-primary-fg-color--light:hsla(var(--md-hue),0%,100%,0.7);--md-primary-fg-color--dark:hsla(var(--md-hue),0%,0%,0.07);--md-primary-bg-color:hsla(var(--md-hue),0%,0%,0.87);--md-primary-bg-color--light:hsla(var(--md-hue),0%,0%,0.54);--md-typeset-a-color:#4051b5}[data-md-color-primary=white] .md-button{color:var(--md-typeset-a-color)}[data-md-color-primary=white] .md-button--primary{background-color:var(--md-typeset-a-color);border-color:var(--md-typeset-a-color);color:hsla(var(--md-hue),0%,100%,1)}@media screen and (min-width:60em){[data-md-color-primary=white] .md-search__form{background-color:hsla(var(--md-hue),0%,0%,.07)}[data-md-color-primary=white] .md-search__form:hover{background-color:hsla(var(--md-hue),0%,0%,.32)}[data-md-color-primary=white] .md-search__input+.md-search__icon{color:hsla(var(--md-hue),0%,0%,.87)}}@media screen and (min-width:76.25em){[data-md-color-primary=white] .md-tabs{border-bottom:.05rem solid #00000012}}[data-md-color-primary=black]{--md-primary-fg-color:hsla(var(--md-hue),15%,9%,1);--md-primary-fg-color--light:hsla(var(--md-hue),15%,9%,0.54);--md-primary-fg-color--dark:hsla(var(--md-hue),15%,9%,1);--md-primary-bg-color:hsla(var(--md-hue),15%,100%,1);--md-primary-bg-color--light:hsla(var(--md-hue),15%,100%,0.7);--md-typeset-a-color:#4051b5}[data-md-color-primary=black] .md-button{color:var(--md-typeset-a-color)}[data-md-color-primary=black] .md-button--primary{background-color:var(--md-typeset-a-color);border-color:var(--md-typeset-a-color);color:hsla(var(--md-hue),0%,100%,1)}[data-md-color-primary=black] .md-header{background-color:hsla(var(--md-hue),15%,9%,1)}@media screen and (max-width:59.984375em){[data-md-color-primary=black] .md-nav__source{background-color:hsla(var(--md-hue),15%,11%,.87)}}@media screen and (max-width:76.234375em){html [data-md-color-primary=black] .md-nav--primary .md-nav__title[for=__drawer]{background-color:hsla(var(--md-hue),15%,9%,1)}}@media screen and (min-width:76.25em){[data-md-color-primary=black] .md-tabs{background-color:hsla(var(--md-hue),15%,9%,1)}}
\ No newline at end of file
diff --git a/v2.2/assets/stylesheets/modern/main.3b031116.min.css b/v2.2/assets/stylesheets/modern/main.3b031116.min.css
new file mode 100644
index 0000000..a3c648f
--- /dev/null
+++ b/v2.2/assets/stylesheets/modern/main.3b031116.min.css
@@ -0,0 +1 @@
+@charset "UTF-8";html{-webkit-text-size-adjust:none;-moz-text-size-adjust:none;text-size-adjust:none;box-sizing:border-box}*,:after,:before{box-sizing:inherit}@media (prefers-reduced-motion){*,:after,:before{transition:none!important}}body{margin:0}a,button,input,label{-webkit-tap-highlight-color:transparent}a{color:inherit;text-decoration:none}hr{border:0;box-sizing:initial;display:block;height:.05rem;overflow:visible;padding:0}small{font-size:80%}sub,sup{line-height:1em}img{border-style:none}table{border-collapse:initial;border-spacing:0}td,th{font-weight:400;vertical-align:top}button{background:#0000;border:0;font-family:inherit;font-size:inherit;margin:0;padding:0}input{border:0;outline:none}:root{--md-primary-fg-color:#4051b5;--md-primary-fg-color--light:#5d6cc0;--md-primary-fg-color--dark:#303fa1;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3;--md-accent-fg-color:#526cfe;--md-accent-fg-color--transparent:#526cfe1a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-scheme=default]{color-scheme:light}[data-md-color-scheme=default] img[src$="#gh-dark-mode-only"],[data-md-color-scheme=default] img[src$="#only-dark"]{display:none}:root,[data-md-color-scheme=default]{--md-hue:225deg;--md-default-fg-color:#000000de;--md-default-fg-color--light:#0000008c;--md-default-fg-color--lighter:#00000052;--md-default-fg-color--lightest:#0000000d;--md-default-bg-color:#fff;--md-default-bg-color--light:#ffffffb3;--md-default-bg-color--lighter:#ffffff4d;--md-default-bg-color--lightest:#ffffff1f;--md-code-fg-color:#36464e;--md-code-bg-color:#f5f5f5;--md-code-bg-color--light:#f5f5f5b3;--md-code-bg-color--lighter:#f5f5f54d;--md-code-hl-color:#4287ff;--md-code-hl-color--light:#4287ff1a;--md-code-hl-number-color:#d52a2a;--md-code-hl-special-color:#db1457;--md-code-hl-function-color:#a846b9;--md-code-hl-constant-color:#6e59d9;--md-code-hl-keyword-color:#3f6ec6;--md-code-hl-string-color:#1c7d4d;--md-code-hl-name-color:var(--md-code-fg-color);--md-code-hl-operator-color:var(--md-default-fg-color--light);--md-code-hl-punctuation-color:var(--md-default-fg-color--light);--md-code-hl-comment-color:var(--md-default-fg-color--light);--md-code-hl-generic-color:var(--md-default-fg-color--light);--md-code-hl-variable-color:var(--md-default-fg-color--light);--md-typeset-color:var(--md-default-fg-color);--md-typeset-a-color:var(--md-primary-fg-color);--md-typeset-del-color:#f5503d26;--md-typeset-ins-color:#0bd57026;--md-typeset-kbd-color:#fafafa;--md-typeset-kbd-accent-color:#fff;--md-typeset-kbd-border-color:#b8b8b8;--md-typeset-mark-color:#ffff0080;--md-typeset-table-color:#0000001f;--md-typeset-table-color--light:rgba(0,0,0,.035);--md-admonition-fg-color:var(--md-default-fg-color);--md-admonition-bg-color:var(--md-default-bg-color);--md-warning-fg-color:#000000de;--md-warning-bg-color:#ff9;--md-shadow-z1:0 0.2rem 0.5rem #0000000d,0 0 0.05rem #0000001a;--md-shadow-z2:0 0.2rem 0.5rem #0000001a,0 0 0.05rem #00000040;--md-shadow-z3:0 0.2rem 0.5rem #0003,0 0 0.05rem #00000059;--color-foreground:0 0 0;--color-background:255 255 255;--color-background-subtle:240 240 240;--color-backdrop:255 255 255}.md-icon svg{fill:currentcolor;display:block;height:1.2rem;width:1.2rem}.md-icon svg.lucide{fill:#0000;stroke:currentcolor}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;--md-text-font-family:var(--md-text-font,_),-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;--md-code-font-family:var(--md-code-font,_),SFMono-Regular,Consolas,Menlo,monospace}aside,body,input{font-feature-settings:"kern","liga";color:var(--md-typeset-color);font-family:var(--md-text-font-family)}code,kbd,pre{font-feature-settings:"kern";font-family:var(--md-code-font-family)}:root{--md-typeset-table-sort-icon:url('data:image/svg+xml;charset=utf-8, ');--md-typeset-table-sort-icon--asc:url('data:image/svg+xml;charset=utf-8, ');--md-typeset-table-sort-icon--desc:url('data:image/svg+xml;charset=utf-8, ');--md-typeset-preview-icon:url('data:image/svg+xml;charset=utf-8, ')}.md-typeset{-webkit-print-color-adjust:exact;color-adjust:exact;font-size:.75rem;letter-spacing:-.01em;line-height:1.8;overflow-wrap:break-word}@media print{.md-typeset{font-size:.68rem}}.md-typeset blockquote,.md-typeset dl,.md-typeset figure,.md-typeset ol,.md-typeset pre,.md-typeset ul{margin-bottom:1em;margin-top:1em}.md-typeset h1{color:var(--md-default-fg-color);font-size:1.875em;line-height:1.3;margin:0 0 1.25em}.md-typeset h1,.md-typeset h2{font-weight:700;letter-spacing:-.025em}.md-typeset h2{font-size:1.5em;line-height:1.4;margin:1.6em 0 .64em}.md-typeset h3{font-size:1.25em;font-weight:700;letter-spacing:-.01em;line-height:1.5;margin:1.6em 0 .8em}.md-typeset h2+h3{margin-top:.8em}.md-typeset h4{font-weight:700;letter-spacing:-.01em;margin:1em 0}.md-typeset h5,.md-typeset h6{color:var(--md-default-fg-color--light);font-size:.8em;font-weight:700;letter-spacing:-.01em;margin:1.25em 0}.md-typeset h5{text-transform:uppercase}.md-typeset h5 code{text-transform:none}.md-typeset hr{border-bottom:.05rem solid var(--md-default-fg-color--lightest);display:flow-root;margin:1.5em 0}.md-typeset a{color:var(--md-typeset-a-color);text-decoration:underline;word-break:break-word}.md-typeset a,.md-typeset a:before{transition:color 125ms}.md-typeset a:focus,.md-typeset a:hover{color:var(--md-accent-fg-color)}.md-typeset a:focus code,.md-typeset a:hover code{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-typeset a code{color:var(--md-typeset-a-color)}.md-typeset a.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-typeset code,.md-typeset kbd,.md-typeset pre{color:var(--md-code-fg-color);direction:ltr;font-variant-ligatures:none;transition:background-color 125ms}@media print{.md-typeset code,.md-typeset kbd,.md-typeset pre{white-space:pre-wrap}}.md-typeset code{background-color:var(--md-code-bg-color);border-radius:.2rem;-webkit-box-decoration-break:clone;box-decoration-break:clone;font-size:.85em;padding:.25em .4em;transition:color 125ms,background-color 125ms;word-break:break-word}.md-typeset code:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}.md-typeset pre{display:flow-root;line-height:1.4;position:relative}.md-typeset pre>code{border-radius:.4rem;-webkit-box-decoration-break:slice;box-decoration-break:slice;box-shadow:none;display:block;margin:0;outline-color:var(--md-accent-fg-color);overflow:auto;padding:.7720588235em 1.1764705882em;scrollbar-color:var(--md-default-fg-color--lighter) #0000;scrollbar-width:thin;touch-action:auto;word-break:normal}.md-typeset pre>code:hover{scrollbar-color:var(--md-accent-fg-color) #0000}.md-typeset pre>code::-webkit-scrollbar{height:.2rem;width:.2rem}.md-typeset pre>code::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-typeset pre>code::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}.md-typeset kbd{border-radius:.2rem;box-shadow:0 0 0 .05rem var(--md-typeset-kbd-border-color),0 .15rem 0 var(--md-typeset-kbd-border-color);color:var(--md-default-fg-color);display:inline-block;font-size:.75em;padding:0 .6666666667em;vertical-align:text-top;word-break:break-word}.md-typeset mark{background-color:var(--md-typeset-mark-color);-webkit-box-decoration-break:clone;box-decoration-break:clone;color:inherit;word-break:break-word}.md-typeset abbr{border-bottom:.05rem dotted var(--md-default-fg-color--light);cursor:help;text-decoration:none}.md-typeset [data-preview]{position:relative}[dir=ltr] .md-typeset [data-preview]:after{margin-left:.125em}[dir=rtl] .md-typeset [data-preview]:after{margin-right:.125em}.md-typeset [data-preview]:after{background-color:currentcolor;content:"";display:inline-block;height:.8em;-webkit-mask-image:var(--md-typeset-preview-icon);mask-image:var(--md-typeset-preview-icon);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:background-color 125ms;vertical-align:text-top;width:.8em}.md-typeset small{opacity:.75}[dir=ltr] .md-typeset sub,[dir=ltr] .md-typeset sup{margin-left:.078125em}[dir=rtl] .md-typeset sub,[dir=rtl] .md-typeset sup{margin-right:.078125em}[dir=ltr] .md-typeset blockquote{padding-left:.6rem}[dir=rtl] .md-typeset blockquote{padding-right:.6rem}[dir=ltr] .md-typeset blockquote{border-left:.2rem solid var(--md-default-fg-color--lighter)}[dir=rtl] .md-typeset blockquote{border-right:.2rem solid var(--md-default-fg-color--lighter)}.md-typeset blockquote{color:var(--md-default-fg-color--light);margin-left:0;margin-right:0}.md-typeset ul{list-style-type:disc}.md-typeset ul[type]{list-style-type:revert-layer}[dir=ltr] .md-typeset ol,[dir=ltr] .md-typeset ul{margin-left:.625em}[dir=rtl] .md-typeset ol,[dir=rtl] .md-typeset ul{margin-right:.625em}.md-typeset ol,.md-typeset ul{padding:0}.md-typeset ol:not([hidden]),.md-typeset ul:not([hidden]){display:flow-root}.md-typeset ol ol,.md-typeset ul ol{list-style-type:lower-alpha}.md-typeset ol ol ol,.md-typeset ul ol ol{list-style-type:lower-roman}.md-typeset ol ol ol ol,.md-typeset ul ol ol ol{list-style-type:upper-alpha}.md-typeset ol ol ol ol ol,.md-typeset ul ol ol ol ol{list-style-type:upper-roman}.md-typeset ol[type],.md-typeset ul[type]{list-style-type:revert-layer}[dir=ltr] .md-typeset ol li,[dir=ltr] .md-typeset ul li{margin-left:1.25em}[dir=rtl] .md-typeset ol li,[dir=rtl] .md-typeset ul li{margin-right:1.25em}.md-typeset ol li,.md-typeset ul li{margin-bottom:.5em}.md-typeset ol li blockquote,.md-typeset ol li p,.md-typeset ul li blockquote,.md-typeset ul li p{margin:.5em 0}.md-typeset ol li:last-child,.md-typeset ul li:last-child{margin-bottom:0}[dir=ltr] .md-typeset ol li ol,[dir=ltr] .md-typeset ol li ul,[dir=ltr] .md-typeset ul li ol,[dir=ltr] .md-typeset ul li ul{margin-left:.625em}[dir=rtl] .md-typeset ol li ol,[dir=rtl] .md-typeset ol li ul,[dir=rtl] .md-typeset ul li ol,[dir=rtl] .md-typeset ul li ul{margin-right:.625em}.md-typeset ol li ol,.md-typeset ol li ul,.md-typeset ul li ol,.md-typeset ul li ul{margin-bottom:.5em;margin-top:.5em}[dir=ltr] .md-typeset dd{margin-left:1.875em}[dir=rtl] .md-typeset dd{margin-right:1.875em}.md-typeset dd{margin-bottom:1.5em;margin-top:1em}.md-typeset img,.md-typeset svg,.md-typeset video{height:auto;max-width:100%}.md-typeset img[align=left]{margin:1em 1em 1em 0}.md-typeset img[align=right]{margin:1em 0 1em 1em}.md-typeset img[align]:only-child{margin-top:0}.md-typeset figure{display:flow-root;margin:1em auto;max-width:100%;text-align:center;width:fit-content}.md-typeset figure img{display:block;margin:0 auto}.md-typeset figcaption{font-style:italic;margin:1em auto;max-width:24rem}.md-typeset iframe{max-width:100%}.md-typeset table:not([class]){background-color:var(--md-default-bg-color);border:.05rem solid var(--md-typeset-table-color);border-radius:.1rem;display:inline-block;font-size:.64rem;max-width:100%;overflow:auto;touch-action:auto}@media print{.md-typeset table:not([class]){display:table}}.md-typeset table:not([class])+*{margin-top:1.5em}.md-typeset table:not([class]) td>:first-child,.md-typeset table:not([class]) th>:first-child{margin-top:0}.md-typeset table:not([class]) td>:last-child,.md-typeset table:not([class]) th>:last-child{margin-bottom:0}.md-typeset table:not([class]) td:not([align]),.md-typeset table:not([class]) th:not([align]){text-align:left}[dir=rtl] .md-typeset table:not([class]) td:not([align]),[dir=rtl] .md-typeset table:not([class]) th:not([align]){text-align:right}.md-typeset table:not([class]) th{font-weight:700;min-width:5rem;padding:.9375em 1.25em;vertical-align:top}.md-typeset table:not([class]) td{border-top:.05rem solid var(--md-typeset-table-color);padding:.9375em 1.25em;vertical-align:top}.md-typeset table:not([class]) tbody tr{transition:background-color 125ms}.md-typeset table:not([class]) tbody tr:hover{background-color:var(--md-typeset-table-color--light);box-shadow:0 .05rem 0 var(--md-default-bg-color) inset}.md-typeset table:not([class]) a{word-break:normal}.md-typeset table th[role=columnheader]{cursor:pointer}[dir=ltr] .md-typeset table th[role=columnheader]:after{margin-left:.5em}[dir=rtl] .md-typeset table th[role=columnheader]:after{margin-right:.5em}.md-typeset table th[role=columnheader]:after{content:"";display:inline-block;height:1.2em;-webkit-mask-image:var(--md-typeset-table-sort-icon);mask-image:var(--md-typeset-table-sort-icon);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:background-color 125ms;vertical-align:text-bottom;width:1.2em}.md-typeset table th[role=columnheader]:hover:after{background-color:var(--md-default-fg-color--lighter)}.md-typeset table th[role=columnheader][aria-sort=ascending]:after{background-color:var(--md-default-fg-color--light);-webkit-mask-image:var(--md-typeset-table-sort-icon--asc);mask-image:var(--md-typeset-table-sort-icon--asc)}.md-typeset table th[role=columnheader][aria-sort=descending]:after{background-color:var(--md-default-fg-color--light);-webkit-mask-image:var(--md-typeset-table-sort-icon--desc);mask-image:var(--md-typeset-table-sort-icon--desc)}.md-typeset__scrollwrap{margin:1em -.8rem;overflow-x:auto;touch-action:auto}.md-typeset__table{display:inline-block;margin-bottom:.5em;padding:0 .8rem}@media print{.md-typeset__table{display:block}}html .md-typeset__table table{display:table;margin:0;overflow:hidden;width:100%}@media screen and (max-width:44.984375em){.md-content__inner>pre{margin:1em -.8rem}.md-content__inner>pre code{border-radius:0}}.md-banner{background-color:var(--md-accent-fg-color--transparent);color:var(--md-default-fg-color);overflow:auto}@media print{.md-banner{display:none}}.md-banner--warning{background-color:var(--md-warning-bg-color);color:var(--md-warning-fg-color)}.md-banner__inner{font-size:.7rem;margin:.6rem auto;padding:0 .8rem}[dir=ltr] .md-banner__button{float:right}[dir=rtl] .md-banner__button{float:left}.md-banner__button{color:inherit;cursor:pointer;transition:opacity .25s}.no-js .md-banner__button{display:none}.md-banner__button:hover{opacity:.7}html{scrollbar-gutter:stable;font-size:125%;height:100%;overflow-x:hidden}@media screen and (min-width:100em){html{font-size:137.5%}}@media screen and (min-width:125em){html{font-size:150%}}body{background-color:var(--md-default-bg-color);display:flex;flex-direction:column;font-size:.5rem;min-height:100%;position:relative;width:100%}@media print{body{display:block}}@media screen and (max-width:59.984375em){body[data-md-scrolllock]{position:fixed}}.md-grid{margin-left:auto;margin-right:auto;max-width:61rem}.md-container{display:flex;flex-direction:column;flex-grow:1}@media print{.md-container{display:block}}.md-main{flex-grow:1}.md-main__inner{display:flex;height:100%;margin-top:1.5rem}.md-ellipsis{overflow:hidden;text-overflow:ellipsis}.md-toggle{display:none}.md-option{height:0;opacity:0;position:absolute;width:0}.md-option:checked+label:not([hidden]){display:block}.md-option.focus-visible+label{outline-color:var(--md-accent-fg-color);outline-style:auto}.md-skip{background-color:var(--md-default-fg-color);border-radius:.1rem;color:var(--md-default-bg-color);font-size:.64rem;margin:.5rem;opacity:0;outline-color:var(--md-accent-fg-color);padding:.3rem .5rem;position:fixed;transform:translateY(.4rem);z-index:-1}.md-skip:focus{opacity:1;transform:translateY(0);transition:transform .25s cubic-bezier(.4,0,.2,1),opacity 175ms 75ms;z-index:10}@page{margin:25mm}:root{--md-clipboard-icon:url('data:image/svg+xml;charset=utf-8, ')}.md-clipboard{border-radius:.1rem;color:var(--md-default-fg-color--lightest);cursor:pointer;height:1.5em;outline-color:var(--md-accent-fg-color);outline-offset:.1rem;transition:color .25s;width:1.5em;z-index:1}@media print{.md-clipboard{display:none}}.md-clipboard:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}:hover>.md-clipboard{color:var(--md-default-fg-color--light)}.md-clipboard:focus,.md-clipboard:hover{color:var(--md-accent-fg-color)}.md-clipboard:after{background-color:currentcolor;content:"";display:block;height:1.125em;margin:0 auto;-webkit-mask-image:var(--md-clipboard-icon);mask-image:var(--md-clipboard-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:1.125em}.md-clipboard--inline{cursor:pointer}.md-clipboard--inline code{transition:color .25s,background-color .25s}.md-clipboard--inline:focus code,.md-clipboard--inline:hover code{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}:root{--md-code-select-icon:url('data:image/svg+xml;charset=utf-8, ');--md-code-copy-icon:url('data:image/svg+xml;charset=utf-8, ')}.md-typeset .md-code__content{display:grid}.md-code__nav{background-color:var(--md-code-bg-color--lighter);border-radius:.1rem;display:flex;gap:.2rem;padding:.2rem;position:absolute;right:.25em;top:.25em;transition:background-color .25s;z-index:1}:hover>.md-code__nav{background-color:var(--md-code-bg-color--light)}.md-code__button{color:var(--md-default-fg-color--lightest);cursor:pointer;display:block;height:1.5em;outline-color:var(--md-accent-fg-color);outline-offset:.1rem;transition:color .25s;width:1.5em}:hover>*>.md-code__button{color:var(--md-default-fg-color--light)}.md-code__button.focus-visible,.md-code__button:hover{color:var(--md-accent-fg-color)}.md-code__button--active{color:var(--md-default-fg-color)!important}.md-code__button:after{background-color:currentcolor;content:"";display:block;height:1.125em;margin:0 auto;-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:1.125em}.md-code__button[data-md-type=select]:after{-webkit-mask-image:var(--md-code-select-icon);mask-image:var(--md-code-select-icon)}.md-code__button[data-md-type=copy]:after{-webkit-mask-image:var(--md-code-copy-icon);mask-image:var(--md-code-copy-icon)}@keyframes consent{0%{opacity:0;transform:translateY(100%)}to{opacity:1;transform:translateY(0)}}@keyframes overlay{0%{opacity:0}to{opacity:1}}.md-consent__overlay{animation:overlay .35s both;-webkit-backdrop-filter:blur(.2rem);backdrop-filter:blur(.2rem);background-color:var(--md-default-bg-color--light);height:100%;opacity:1;position:fixed;top:0;width:100%;z-index:5}.md-consent__inner{bottom:0;display:flex;justify-content:center;max-height:100%;padding:0;position:fixed;width:100%;z-index:5}.md-consent__form{animation:consent .5s cubic-bezier(.1,.7,.1,1) both;background-color:var(--md-default-bg-color);border:0;border-radius:.8rem;box-shadow:var(--md-shadow-z3);margin:.4rem;overflow:auto;padding-left:1.2rem;padding-right:1.2rem}.md-consent__settings{display:none;margin:1em 0}input:checked+.md-consent__settings{display:block}.md-consent__controls{line-height:1.2;margin-bottom:.8rem}.md-typeset .md-consent__controls .md-button{display:inline}@media screen and (max-width:44.984375em){.md-typeset .md-consent__controls .md-button{display:block;margin-top:.4rem;text-align:center;width:100%}}.md-consent label{cursor:pointer}.md-content{flex-grow:1;min-width:0}.md-content__inner{margin:0 .8rem 1.2rem;padding-top:.7rem}@media screen and (min-width:76.25em){[dir=ltr] .md-sidebar--primary:not([hidden])~.md-content>.md-content__inner{margin-left:1.2rem}[dir=ltr] .md-sidebar--secondary:not([hidden])~.md-content>.md-content__inner,[dir=rtl] .md-sidebar--primary:not([hidden])~.md-content>.md-content__inner{margin-right:1.2rem}[dir=rtl] .md-sidebar--secondary:not([hidden])~.md-content>.md-content__inner{margin-left:1.2rem}}.md-content__inner:before{content:"";display:block;height:.4rem}.md-content__inner>:last-child{margin-bottom:0}[dir=ltr] .md-content__button{float:right}[dir=rtl] .md-content__button{float:left}[dir=ltr] .md-content__button{margin-left:.4rem}[dir=rtl] .md-content__button{margin-right:.4rem}.md-content__button{background-color:var(--md-default-fg-color--lightest);border-radius:.4rem;display:flex;margin-top:.2rem;padding:.3rem}@media print{.md-content__button{display:none}}.md-typeset .md-content__button{color:var(--md-default-fg-color);transition:color .25s,background-color .25s}.md-typeset .md-content__button svg{opacity:.5;transition:opacity .25s}.md-typeset .md-content__button:focus,.md-typeset .md-content__button:hover{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-typeset .md-content__button:focus svg,.md-typeset .md-content__button:hover svg{opacity:1}.md-content__button svg{height:.9rem;width:.9rem}[dir=rtl] .md-content__button svg{transform:scaleX(-1)}.md-content__button svg.lucide{fill:#0000;stroke:currentcolor}[dir=ltr] .md-dialog{right:.8rem}[dir=rtl] .md-dialog{left:.8rem}.md-dialog{background-color:var(--md-accent-fg-color);border-radius:1.2rem;bottom:.8rem;box-shadow:var(--md-shadow-z3);min-width:11.1rem;opacity:0;padding:.4rem 1.2rem;pointer-events:none;position:fixed;transform:translateY(100%);transition:transform 0ms .4s,opacity .4s;z-index:4}@media print{.md-dialog{display:none}}.md-dialog--active{opacity:1;pointer-events:auto;transform:translateY(0);transition:transform .4s cubic-bezier(.075,.85,.175,1),opacity .4s}.md-dialog__inner{color:var(--md-default-bg-color);font-size:.7rem}.md-feedback{margin:2em 0 1em;text-align:center}.md-feedback fieldset{border:none;margin:0;padding:0}.md-feedback__title{font-weight:700;margin:1em auto}.md-feedback__inner{position:relative}.md-feedback__list{display:flex;flex-wrap:wrap;place-content:baseline center;position:relative}.md-feedback__list:hover .md-icon:not(:disabled){color:var(--md-default-fg-color--lighter)}:disabled .md-feedback__list{min-height:1.8rem}.md-feedback__icon{color:var(--md-default-fg-color--light);cursor:pointer;flex-shrink:0;margin:0 .1rem;transition:color 125ms}.md-feedback__icon:not(:disabled).md-icon:hover{color:var(--md-accent-fg-color)}.md-feedback__icon:disabled{color:var(--md-default-fg-color--lightest);pointer-events:none}.md-feedback__note{opacity:0;position:relative;transform:translateY(.4rem);transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .15s}.md-feedback__note>*{margin:0 auto;max-width:16rem}:disabled .md-feedback__note{opacity:1;transform:translateY(0)}@media print{.md-feedback{display:none}}.md-footer{background-color:var(--md-default-bg-color);border-top:.05rem solid var(--md-default-fg-color--lightest);color:var(--md-default-fg-color)}@media print{.md-footer{display:none}}.md-footer__inner{justify-content:space-between;overflow:auto;padding:.2rem}.md-footer__inner:not([hidden]){display:flex}.md-footer__link{align-items:end;display:flex;flex-grow:0.01;margin-bottom:.4rem;margin-top:1rem;max-width:100%;outline-color:var(--md-accent-fg-color);overflow:hidden;transition:opacity .25s}.md-footer__link:focus,.md-footer__link:hover{opacity:.7}[dir=rtl] .md-footer__link svg{transform:scaleX(-1)}@media screen and (max-width:44.984375em){.md-footer__link--prev{flex-shrink:0}.md-footer__link--prev .md-footer__title{display:none}}[dir=ltr] .md-footer__link--next{margin-left:auto}[dir=rtl] .md-footer__link--next{margin-right:auto}.md-footer__link--next{text-align:right}[dir=rtl] .md-footer__link--next{text-align:left}.md-footer__title{flex-grow:1;font-size:.8rem;margin-bottom:.7rem;max-width:calc(100% - 2.4rem);padding:0 1rem;white-space:nowrap}.md-footer__button{margin:.2rem;padding:.4rem}.md-footer__direction{font-size:.6rem;opacity:.7}.md-footer-meta{background-color:var(--md-default-fg-color--lightest)}.md-footer-meta__inner{display:flex;flex-wrap:wrap;justify-content:space-between;padding:.2rem}html .md-footer-meta.md-typeset a:not(:focus,:hover){color:var(--md-default-fg-color)}.md-copyright{color:var(--md-default-fg-color--light);font-size:.64rem;margin:auto .6rem;padding:.4rem 0;width:100%}@media screen and (min-width:45em){.md-copyright{width:auto}}.md-copyright__highlight{color:var(--md-default-fg-color)}.md-social{display:inline-flex;gap:.2rem;margin:0 .4rem;padding:.2rem 0 .6rem}@media screen and (min-width:45em){.md-social{padding:.6rem 0}}.md-social__link{display:inline-block;height:1.6rem;text-align:center;width:1.6rem}.md-social__link:before{line-height:1.9}.md-social__link svg{fill:currentcolor;max-height:.8rem;vertical-align:-25%}.md-social__link svg.lucide{fill:#0000;stroke:currentcolor}.md-typeset .md-button{background-color:var(--md-default-fg-color--lightest);border-radius:1.2rem;color:var(--md-default-fg-color--light);cursor:pointer;display:inline-block;font-size:.875em;font-weight:700;padding:.625em 2em;text-decoration:none;transition:color 125ms,background-color 125ms,opacity 125ms}.md-typeset .md-button.focus-visible{outline-offset:0}.md-typeset .md-button:focus,.md-typeset .md-button:hover{color:var(--md-default-fg-color--light);opacity:.8}.md-typeset .md-button--primary{background-color:var(--md-primary-fg-color);color:var(--md-primary-bg-color)}.md-typeset .md-button--primary:focus,.md-typeset .md-button--primary:hover{color:var(--md-primary-bg-color);opacity:.8}[dir=ltr] .md-typeset .md-input{border-top-left-radius:.1rem}[dir=ltr] .md-typeset .md-input,[dir=rtl] .md-typeset .md-input{border-top-right-radius:.1rem}[dir=rtl] .md-typeset .md-input{border-top-left-radius:.1rem}.md-typeset .md-input{border-bottom:.1rem solid var(--md-default-fg-color--lighter);box-shadow:var(--md-shadow-z1);font-size:.8rem;height:1.8rem;padding:0 .6rem;transition:border .25s,box-shadow .25s}.md-typeset .md-input:focus,.md-typeset .md-input:hover{border-bottom-color:var(--md-accent-fg-color);box-shadow:var(--md-shadow-z2)}.md-typeset .md-input--stretch{width:100%}.md-header{-webkit-backdrop-filter:blur(.4rem);backdrop-filter:blur(.4rem);background-color:var(--md-default-bg-color--light);color:var(--md-default-fg-color);display:block;left:0;position:sticky;right:0;top:0;z-index:4}@media print{.md-header{display:none}}.md-header[hidden]{transform:translateY(-100%);transition:transform .25s cubic-bezier(.8,0,.6,1)}.md-header--shadow{box-shadow:0 .05rem 0 var(--md-default-fg-color--lightest);transition:transform .25s cubic-bezier(.1,.7,.1,1)}.md-header__inner{align-items:center;display:flex;padding:0 .4rem}.md-header__button{color:currentcolor;cursor:pointer;margin:.2rem;outline-color:var(--md-accent-fg-color);padding:.4rem;position:relative;transition:opacity .25s;vertical-align:middle;z-index:1}.md-header__button:hover{opacity:.7}.md-header__button:not([hidden]){display:inline-block}.md-header__button:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}.md-header__button.md-logo{margin:.2rem;padding:.4rem}@media screen and (max-width:76.234375em){.md-header__button.md-logo{display:none}}.md-header__button.md-logo img,.md-header__button.md-logo svg{fill:currentcolor;display:block;height:1.2rem;width:auto}.md-header__button.md-logo img.lucide,.md-header__button.md-logo svg.lucide{fill:#0000;stroke:currentcolor}@media screen and (min-width:60em){.md-header__button[for=__search]{display:none}}.no-js .md-header__button[for=__search]{display:none}[dir=rtl] .md-header__button[for=__search] svg{transform:scaleX(-1)}@media screen and (min-width:76.25em){.md-header__button[for=__drawer]{display:none}}.md-header__topic{display:flex;max-width:100%;position:absolute;transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .15s;white-space:nowrap}.md-header__topic+.md-header__topic{opacity:0;pointer-events:none;transform:translateX(1.25rem);transition:transform .4s cubic-bezier(1,.7,.1,.1),opacity .15s;z-index:-1}[dir=rtl] .md-header__topic+.md-header__topic{transform:translateX(-1.25rem)}.md-header__topic:first-child{font-weight:700}.md-header__title{flex-grow:1;font-size:.9rem;height:2.4rem;letter-spacing:-.025em;line-height:2.4rem;margin-left:.4rem;margin-right:.4rem}.md-header__title--active .md-header__topic{opacity:0;pointer-events:none;transform:translateX(-1.25rem);transition:transform .4s cubic-bezier(1,.7,.1,.1),opacity .15s;z-index:-1}[dir=rtl] .md-header__title--active .md-header__topic{transform:translateX(1.25rem)}.md-header__title--active .md-header__topic+.md-header__topic{opacity:1;pointer-events:auto;transform:translateX(0);transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .15s;z-index:0}.md-header__title>.md-header__ellipsis{height:100%;position:relative;width:100%}.md-header__option{display:flex;flex-shrink:0;max-width:100%;white-space:nowrap}.md-header__option>input{bottom:0}.md-header__source{display:none}@media screen and (min-width:60em){[dir=ltr] .md-header__source{margin-left:1rem}[dir=rtl] .md-header__source{margin-right:1rem}.md-header__source{display:block;max-width:11.5rem;width:11.5rem}}@media screen and (min-width:76.25em){[dir=ltr] .md-header__source{margin-left:1.4rem}[dir=rtl] .md-header__source{margin-right:1.4rem}}.md-header .md-icon svg{height:1rem;width:1rem}:root{--md-nav-icon--next:url('data:image/svg+xml;charset=utf-8, ')}.md-nav{font-size:.7rem;line-height:1.3;transition:max-height .25s cubic-bezier(.86,0,.07,1)}.md-nav .md-nav__title{display:none}.md-nav__list{display:flex;flex-direction:column;gap:.2rem;list-style:none;margin:0;padding:0}[dir=ltr] .md-nav__list .md-nav__list{margin-left:.6rem}[dir=rtl] .md-nav__list .md-nav__list{margin-right:.6rem}.md-nav__item--nested .md-nav__list:after,.md-nav__item--nested .md-nav__list:before{content:" ";display:block;height:0}.md-nav__link{align-items:flex-start;border-radius:.4rem;cursor:pointer;display:flex;gap:.6rem;margin-left:.2rem;margin-right:.2rem;padding:.35rem .8rem;transition:color .25s,background-color .25s}.md-nav__link .md-nav__link{margin:0}.md-nav__link--passed,.md-nav__link--passed code{color:var(--md-default-fg-color--light)}.md-nav__item .md-nav__link--active{font-weight:500}.md-nav--primary .md-nav__item .md-nav__link--active{background:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-nav__item .md-nav__link--active,.md-nav__item .md-nav__link--active code{color:var(--md-typeset-a-color)}.md-nav__item .md-nav__link--active code svg,.md-nav__item .md-nav__link--active svg{opacity:1}[dir=ltr] .md-nav__item--nested>.md-nav__link:not(.md-nav__container){padding-right:.35rem}[dir=rtl] .md-nav__item--nested>.md-nav__link:not(.md-nav__container){padding-left:.35rem}.md-nav__link .md-ellipsis{flex-grow:1;position:relative}.md-nav__link .md-ellipsis code{word-break:normal}.md-nav__link svg{fill:currentcolor;flex-shrink:0;height:1.3em;opacity:.5;position:relative;width:1.3em}.md-nav__link svg.lucide{fill:#0000;stroke:currentcolor}.md-nav--primary .md-nav__link[for]:focus:not(.md-nav__link--active),.md-nav--primary .md-nav__link[for]:hover:not(.md-nav__link--active),.md-nav--primary .md-nav__link[href]:focus:not(.md-nav__link--active),.md-nav--primary .md-nav__link[href]:hover:not(.md-nav__link--active){background-color:var(--md-default-fg-color--lightest);color:var(--md-default-fg-color)}.md-nav--secondary .md-nav__link{margin-left:.2rem;margin-right:.2rem;padding:.35rem .8rem}.md-nav--secondary .md-nav__link[for]:focus,.md-nav--secondary .md-nav__link[for]:hover,.md-nav--secondary .md-nav__link[href]:focus,.md-nav--secondary .md-nav__link[href]:hover{background-color:initial;color:var(--md-accent-fg-color)}.md-nav__link.focus-visible{outline-color:var(--md-accent-fg-color)}.md-nav--primary .md-nav__link[for=__toc],.md-nav--primary .md-nav__link[for=__toc]~.md-nav{display:none}.md-nav__icon{font-size:.9rem;height:.9rem;width:.9rem}[dir=rtl] .md-nav__icon:after{transform:rotate(180deg)}.md-nav__item--nested .md-nav__icon:after{background-color:currentcolor;content:"";display:block;height:100%;-webkit-mask-image:var(--md-nav-icon--next);mask-image:var(--md-nav-icon--next);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:transform .25s;width:100%}@media screen and (min-width:76.25em){.md-nav__item--nested.md-nav__item--section>.md-nav__link .md-nav__icon:after{display:none}}.md-nav__item--nested .md-nav__toggle:checked~.md-nav__link .md-nav__icon:after,.md-nav__item--nested .md-toggle--indeterminate~.md-nav__link .md-nav__icon:after{transform:rotate(90deg)}.md-nav__container{background:#0000;gap:.2rem;padding:0}.md-nav__container>:first-child{flex-grow:1;min-width:0}.md-nav__container>:nth-child(2){padding:.35rem}@media screen and (min-width:76.25em){.md-nav__item--section>.md-nav__container>:nth-child(2){display:none}}.md-nav__container__icon{flex-shrink:0}.md-nav__toggle~.md-nav{display:grid;grid-template-rows:minmax(.005rem,0fr);opacity:0;transition:grid-template-rows .25s cubic-bezier(.86,0,.07,1),opacity .25s,visibility 0ms .25s;visibility:collapse}.md-nav__toggle~.md-nav>.md-nav__list{overflow:hidden}.md-nav__toggle.md-toggle--indeterminate~.md-nav,.md-nav__toggle:checked~.md-nav{grid-template-rows:minmax(.4rem,1fr);opacity:1;transition:grid-template-rows .25s cubic-bezier(.86,0,.07,1),opacity .15s .1s,visibility 0ms;visibility:visible}.md-nav__toggle.md-toggle--indeterminate~.md-nav{transition:none}.md-nav--secondary{margin-bottom:.1rem;margin-top:.1rem}.md-nav--secondary .md-nav{margin-top:.2rem}.md-nav--secondary .md-nav__title{background:var(--md-default-bg-color);display:flex;font-weight:700;margin-left:.2rem;margin-right:.2rem;padding:.35rem .6rem;position:sticky;top:0;z-index:1}.md-nav--secondary .md-nav__title .md-nav__icon{display:none}.md-nav--secondary .md-nav__link{padding:.2rem .6rem}@media screen and (max-width:76.234375em){.md-nav--primary{margin-bottom:.4rem;margin-left:.2rem;margin-right:.2rem}.md-nav .md-nav__title[for=__drawer]{align-items:center;border-bottom:.05rem solid var(--md-default-fg-color-lightest);display:flex;font-size:.8rem;font-weight:700;gap:.4rem;padding:.8rem}.md-nav .md-nav__title[for=__drawer] .md-logo{height:1.6rem;width:1.6rem}.md-nav .md-nav__title[for=__drawer] .md-logo img,.md-nav .md-nav__title[for=__drawer] .md-logo svg{fill:currentcolor;display:block;height:100%;max-width:100%;object-fit:contain;width:auto}.md-nav .md-nav__title[for=__drawer] .md-logo img.lucide,.md-nav .md-nav__title[for=__drawer] .md-logo svg.lucide{fill:#0000;stroke:currentcolor}}.md-nav__source{border:.05rem solid var(--md-default-fg-color--lightest);border-radius:.4rem;margin:.2rem .2rem .6rem;transition:background-color .25s,border-color .25s}.md-nav__source:focus,.md-nav__source:hover{background-color:var(--md-default-fg-color--lightest);border-color:#0000}[dir=ltr] .md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{margin-left:1.1rem}[dir=rtl] .md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{margin-right:1.1rem}[dir=ltr] .md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{border-left:.05rem solid var(--md-default-fg-color--lightest)}[dir=rtl] .md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{border-right:.05rem solid var(--md-default-fg-color--lightest)}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{display:block;margin-bottom:.5em;margin-top:.5em;opacity:1;visibility:visible}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary .md-nav__link{background:#0000}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary .md-nav__link--active{font-weight:500}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary .md-nav__link:focus,.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary .md-nav__link:hover{color:var(--md-accent-fg-color)}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary>.md-nav__list{margin-left:0;overflow:visible;padding-bottom:0}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary>.md-nav__title{display:none}@media screen and (min-width:76.25em){.md-nav--primary{margin-bottom:.1rem;margin-top:.1rem}.md-nav__source{display:none}[dir=ltr] .md-nav__list .md-nav__item--section>.md-nav>.md-nav__list{margin-left:0}[dir=rtl] .md-nav__list .md-nav__item--section>.md-nav>.md-nav__list{margin-right:0}.md-nav__item--section>.md-nav__link--active,.md-nav__item--section>.md-nav__link>.md-nav__link--active{font-weight:700}.md-nav__item--section{margin-top:.4rem}.md-nav__item--section:first-child{margin-top:0}.md-nav__item--section:last-child{margin-bottom:0}.md-nav__item--section>.md-nav__link{font-weight:700}.md-nav__item--section>.md-nav__link:not(.md-nav__container){pointer-events:none}.md-nav__item--section>.md-nav{display:block;opacity:1;visibility:visible}.md-nav__item--section>.md-nav>.md-nav__list>.md-nav__item{padding:0}.md-nav--lifted{margin-top:0}.md-nav--lifted>.md-nav__list>.md-nav__item{display:none}.md-nav--lifted>.md-nav__list>.md-nav__item--active{display:block}.md-nav--lifted>.md-nav__list>.md-nav__item--active>.md-nav{margin-top:.1rem}.md-nav--lifted>.md-nav__list>.md-nav__item--active>.md-nav>.md-nav__list:before,.md-nav--lifted>.md-nav__list>.md-nav__item--active>.md-nav__link{display:none}.md-nav--lifted>.md-nav__list>.md-nav__item--active.md-nav__item--section{margin:0}.md-nav--lifted .md-nav[data-md-level="1"]{grid-template-rows:minmax(.4rem,1fr);opacity:1;visibility:visible}}:root{--md-path-icon:url('data:image/svg+xml;charset=utf-8, ')}.md-path{font-size:.7rem;margin:.4rem .8rem 0;overflow:auto;padding-top:1.2rem}.md-path:not([hidden]){display:block}@media screen and (min-width:76.25em){.md-path{margin:.4rem 1.2rem 0}}.md-path__list{align-items:center;display:flex;gap:.2rem;list-style:none;margin:0;padding:0}.md-path__item:not(:first-child){align-items:center;display:inline-flex;gap:.2rem;white-space:nowrap}.md-path__item:not(:first-child):before{background-color:var(--md-default-fg-color--lighter);content:"";display:inline;height:.6rem;-webkit-mask-image:var(--md-path-icon);mask-image:var(--md-path-icon);width:.6rem}.md-path__link{align-items:center;color:var(--md-default-fg-color--light);display:flex;transition:color .25s}.md-path__link:focus,.md-path__link:hover{color:var(--md-accent-fg-color)}:root{--md-progress-value:0;--md-progress-delay:400ms}.md-progress{background:var(--md-primary-bg-color);height:.075rem;opacity:min(clamp(0,var(--md-progress-value),1),clamp(0,100 - var(--md-progress-value),1));position:fixed;top:0;transform:scaleX(calc(var(--md-progress-value)*1%));transform-origin:left;transition:transform .5s cubic-bezier(.19,1,.22,1),opacity .25s var(--md-progress-delay);width:100%;z-index:4}:root{--md-search-icon:url('data:image/svg+xml;charset=utf-8, ')}.md-search{position:relative}@media screen and (min-width:45em){.md-search{padding:.2rem 0}}@media screen and (max-width:59.984375em){.md-search{display:none}}.no-js .md-search{display:none}[dir=ltr] .md-search__button{padding-left:1.9rem;padding-right:2.2rem}[dir=rtl] .md-search__button{padding-left:2.2rem;padding-right:1.9rem}.md-search__button{background:var(--md-default-bg-color);color:var(--md-default-fg-color);cursor:pointer;font-size:.7rem;position:relative;text-align:left}@media screen and (min-width:45em){.md-search__button{background-color:var(--md-default-fg-color--lightest);border-radius:.4rem;height:1.6rem;transition:background-color .4s,color .4s;width:8.9rem}.md-search__button:focus,.md-search__button:hover{background-color:var(--md-default-fg-color--lighter);color:var(--md-default-fg-color)}}[dir=ltr] .md-search__button:before{left:0}[dir=rtl] .md-search__button:before{right:0}.md-search__button:before{background-color:var(--md-default-fg-color);content:"";height:1rem;margin-left:.5rem;-webkit-mask-image:var(--md-search-icon);mask-image:var(--md-search-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.3rem;width:1rem}.md-search__button:after{background:var(--md-default-bg-color--light);border-radius:.2rem;content:"Ctrl+K";display:block;font-size:.6rem;padding:.1rem .2rem;position:absolute;right:.6rem;top:.35rem}[data-platform^=Mac] .md-search__button:after{content:"⌘K"}.md-select{position:relative;z-index:1}.md-select__inner{background-color:var(--md-default-bg-color);border-radius:.4rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color);left:50%;margin-top:.2rem;max-height:0;opacity:0;position:absolute;top:calc(100% - .2rem);transform:translate3d(-50%,.3rem,0);transition:transform .25s 375ms,opacity .25s .25s,max-height 0ms .5s}@media screen and (max-width:59.984375em){.md-select__inner{left:100%;transform:translate3d(-100%,.3rem,0)}}.md-select:focus-within .md-select__inner,.md-select:hover .md-select__inner{max-height:min(75vh,28rem);opacity:1;transform:translate3d(-50%,0,0);transition:transform .25s cubic-bezier(.1,.7,.1,1),opacity .25s,max-height 0ms}@media screen and (max-width:59.984375em){.md-select:focus-within .md-select__inner,.md-select:hover .md-select__inner{transform:translate3d(-100%,0,0)}}.md-select__inner:after{border-bottom:.2rem solid #0000;border-bottom-color:var(--md-default-bg-color);border-left:.2rem solid #0000;border-right:.2rem solid #0000;border-top:0;content:"";filter:drop-shadow(0 -1px 0 var(--md-default-fg-color--lightest));height:0;left:50%;margin-left:-.2rem;margin-top:-.2rem;position:absolute;top:0;width:0}@media screen and (max-width:59.984375em){.md-select__inner:after{left:auto;right:1rem}}.md-select__list{border-radius:.1rem;font-size:.8rem;list-style-type:none;margin:0;max-height:inherit;overflow:auto;padding:0}.md-select__item{line-height:1.8rem}[dir=ltr] .md-select__link{padding-left:.6rem;padding-right:1.2rem}[dir=rtl] .md-select__link{padding-left:1.2rem;padding-right:.6rem}.md-select__link{cursor:pointer;display:block;outline:none;scroll-snap-align:start;transition:background-color .25s,color .25s;width:100%}.md-select__link:focus,.md-select__link:hover{color:var(--md-accent-fg-color)}.md-select__link:focus{background-color:var(--md-default-fg-color--lightest)}:root{--md-toc-icon:url('data:image/svg+xml;charset=utf-8, ')}.md-sidebar{align-self:flex-start;flex-shrink:0;padding:1.1rem 0;position:sticky;top:2.4rem;width:12.1rem}@media print{.md-sidebar{display:none}}@media screen and (max-width:76.234375em){[dir=ltr] .md-sidebar--primary{left:-12.1rem}[dir=rtl] .md-sidebar--primary{right:-12.1rem}.md-sidebar--primary{-webkit-backdrop-filter:blur(.4rem);backdrop-filter:blur(.4rem);background-color:var(--md-default-bg-color--light);border-radius:.8rem;display:block;height:calc(100% - .8rem);position:fixed;top:.4rem;transform:translateX(0);transition:transform .2s cubic-bezier(.5,0,.5,0),box-shadow .2s;width:12.1rem;z-index:5}[data-md-toggle=drawer]:checked~.md-container .md-sidebar--primary{box-shadow:var(--md-shadow-z3);transform:translateX(12.5rem);transition:transform .25s cubic-bezier(.7,.7,.1,1),box-shadow .25s}[dir=rtl] [data-md-toggle=drawer]:checked~.md-container .md-sidebar--primary{transform:translateX(-12.5rem)}.md-sidebar--primary .md-sidebar__scrollwrap{bottom:0;left:0;margin:0;overscroll-behavior-y:contain;position:absolute;right:0;top:0}}@media screen and (min-width:76.25em){.md-sidebar{height:0}.no-js .md-sidebar{height:auto}.md-header--lifted~.md-container .md-sidebar{top:4.8rem}}.md-sidebar--secondary{order:2}@media screen and (max-width:59.984375em){.md-sidebar--secondary{bottom:1.6rem;padding:0;position:fixed;right:.8rem;top:auto;width:auto;z-index:2}.md-sidebar--secondary .md-nav--secondary{margin-top:0}.md-sidebar--secondary .md-nav__title{padding:.55rem .6rem .35rem}.md-sidebar--secondary .md-sidebar__scrollwrap{display:flex;flex-direction:column-reverse;overflow-y:visible;position:relative}.md-sidebar--secondary .md-sidebar__inner{background-color:var(--md-default-bg-color);border-radius:.4rem;bottom:2.7rem;box-shadow:var(--md-shadow-z2);max-height:50vh;opacity:0;overflow-y:auto;padding-bottom:.4rem;pointer-events:none;position:absolute;right:0;transform:translateY(.4rem);transition:transform 0ms .25s,opacity .25s;width:11.7rem}.md-sidebar--secondary [type=checkbox]:checked~.md-sidebar__inner{opacity:1;pointer-events:auto;transform:translateY(0);transition:transform .4s cubic-bezier(0,1,.35,1),opacity .25s,z-index 0ms}.md-sidebar--secondary .md-sidebar-button{-webkit-backdrop-filter:blur(.4rem);backdrop-filter:blur(.4rem);background-color:var(--md-default-bg-color--light);border-radius:1.6rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color--light);cursor:pointer;display:inline-flex;font-size:.7rem;gap:.4rem;outline:none;padding:.5rem;transition:color 125ms,background-color 125ms,transform 125ms cubic-bezier(.4,0,.2,1),opacity 125ms}.md-sidebar--secondary .md-sidebar-button:after{background-color:currentcolor;content:"";display:block;height:.9rem;-webkit-mask-image:var(--md-toc-icon);mask-image:var(--md-toc-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:transform .25s;width:.9rem}.md-sidebar--secondary .md-sidebar-button:focus,.md-sidebar--secondary .md-sidebar-button:hover{background-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}.md-sidebar--secondary .md-sidebar-button__wrapper{text-align:right}}@media screen and (min-width:60em){.md-sidebar--secondary{height:0}.md-sidebar--secondary .md-sidebar-button{display:none}.no-js .md-sidebar--secondary{height:auto}.md-sidebar--secondary:not([hidden]){display:block}.md-sidebar--secondary .md-sidebar__scrollwrap{touch-action:pan-y}}.md-sidebar__scrollwrap{backface-visibility:hidden;overflow-y:auto;scrollbar-color:var(--md-default-fg-color--lighter) #0000}@media screen and (min-width:60em){.md-sidebar__scrollwrap{scrollbar-gutter:stable;scrollbar-width:thin}}.md-sidebar__scrollwrap::-webkit-scrollbar{height:.2rem;width:.2rem}.md-sidebar__scrollwrap:focus-within,.md-sidebar__scrollwrap:hover{scrollbar-color:var(--md-accent-fg-color) #0000}.md-sidebar__scrollwrap:focus-within::-webkit-scrollbar-thumb,.md-sidebar__scrollwrap:hover::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-sidebar__scrollwrap:focus-within::-webkit-scrollbar-thumb:hover,.md-sidebar__scrollwrap:hover::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}@supports selector(::-webkit-scrollbar){.md-sidebar__scrollwrap{scrollbar-gutter:auto}[dir=ltr] .md-sidebar__inner{padding-right:calc(100% - 11.5rem)}[dir=rtl] .md-sidebar__inner{padding-left:calc(100% - 11.5rem)}@media screen and (max-width:76.234375em){[dir=ltr] .md-sidebar__inner{padding-right:0}[dir=rtl] .md-sidebar__inner{padding-left:0}}}@media screen and (max-width:76.234375em){.md-overlay{-webkit-backdrop-filter:blur(.2rem);backdrop-filter:blur(.2rem);background-color:var(--md-default-bg-color--light);height:0;opacity:0;position:fixed;top:0;transition:width 0ms .5s,height 0ms .5s,opacity .25s 125ms;width:0;z-index:5}[data-md-toggle=drawer]:checked~.md-overlay{height:100%;opacity:1;transition:width 0ms,height 0ms,opacity .25s;width:100%}}@keyframes facts{0%{height:0}to{height:.65rem}}@keyframes fact{0%{opacity:0;transform:translateY(100%)}50%{opacity:0}to{opacity:1;transform:translateY(0)}}:root{--md-source-forks-icon:url('data:image/svg+xml;charset=utf-8, ');--md-source-repositories-icon:url('data:image/svg+xml;charset=utf-8, ');--md-source-stars-icon:url('data:image/svg+xml;charset=utf-8, ');--md-source-version-icon:url('data:image/svg+xml;charset=utf-8, ')}.md-source{backface-visibility:hidden;display:block;font-size:.55rem;line-height:1.2;outline-color:var(--md-accent-fg-color);transition:opacity .25s;white-space:nowrap}.md-source:hover{opacity:.7}.md-source__icon{display:inline-block;height:2.4rem;vertical-align:middle;width:2rem}[dir=ltr] .md-source__icon svg{margin-left:.6rem}[dir=rtl] .md-source__icon svg{margin-right:.6rem}.md-source__icon svg{margin-top:.6rem}.md-header .md-source__icon svg{height:1.2rem;width:1.2rem}[dir=ltr] .md-source__icon+.md-source__repository{padding-left:2rem}[dir=rtl] .md-source__icon+.md-source__repository{padding-right:2rem}[dir=ltr] .md-source__icon+.md-source__repository{margin-left:-2rem}[dir=rtl] .md-source__icon+.md-source__repository{margin-right:-2rem}[dir=ltr] .md-source__repository{margin-left:.6rem}[dir=rtl] .md-source__repository{margin-right:.6rem}.md-source__repository{display:inline-block;max-width:calc(100% - 1.2rem);overflow:hidden;text-overflow:ellipsis;vertical-align:middle}.md-source__facts{display:flex;font-size:.55rem;gap:.4rem;list-style-type:none;margin:.1rem 0 0;opacity:.75;overflow:hidden;padding:0;width:100%}.md-source__repository--active .md-source__facts{animation:facts 0ms ease-in}.md-source__fact{overflow:hidden;text-overflow:ellipsis}.md-source__repository--active .md-source__fact{animation:fact 0ms ease-out}[dir=ltr] .md-source__fact:before{margin-right:.1rem}[dir=rtl] .md-source__fact:before{margin-left:.1rem}.md-source__fact:before{background-color:currentcolor;content:"";display:inline-block;height:.6rem;-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;vertical-align:text-top;width:.6rem}.md-source__fact:nth-child(1n+2){flex-shrink:0}.md-source__fact--version:before{-webkit-mask-image:var(--md-source-version-icon);mask-image:var(--md-source-version-icon)}.md-source__fact--stars:before{-webkit-mask-image:var(--md-source-stars-icon);mask-image:var(--md-source-stars-icon)}.md-source__fact--forks:before{-webkit-mask-image:var(--md-source-forks-icon);mask-image:var(--md-source-forks-icon)}.md-source__fact--repositories:before{-webkit-mask-image:var(--md-source-repositories-icon);mask-image:var(--md-source-repositories-icon)}.md-source-file{margin:1em 0}[dir=ltr] .md-source-file__fact{margin-right:.6rem}[dir=rtl] .md-source-file__fact{margin-left:.6rem}.md-source-file__fact{align-items:center;color:var(--md-default-fg-color--light);display:inline-flex;font-size:.68rem;gap:.3rem}.md-source-file__fact .md-icon{flex-shrink:0;margin-bottom:.05rem}[dir=ltr] .md-source-file__fact .md-author{float:left}[dir=rtl] .md-source-file__fact .md-author{float:right}.md-source-file__fact .md-author{margin-right:.2rem}.md-source-file__fact svg{width:.9rem}:root{--md-status:url('data:image/svg+xml;charset=utf-8, ');--md-status--new:url('data:image/svg+xml;charset=utf-8, ');--md-status--deprecated:url('data:image/svg+xml;charset=utf-8, ')}.md-status:after{background-color:var(--md-default-fg-color--light);content:"";display:inline-block;height:1.125em;-webkit-mask-image:var(--md-status);mask-image:var(--md-status);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;vertical-align:text-bottom;width:1.125em}.md-status:hover:after{background-color:currentcolor}.md-status--new:after{-webkit-mask-image:var(--md-status--new);mask-image:var(--md-status--new)}.md-status--deprecated:after{-webkit-mask-image:var(--md-status--deprecated);mask-image:var(--md-status--deprecated)}.md-tabs{box-shadow:0 -.05rem 0 inset var(--md-default-fg-color--lightest);color:var(--md-default-fg-color);display:block;line-height:1.3;overflow:auto;width:100%;z-index:2}@media print{.md-tabs{display:none}}@media screen and (max-width:76.234375em){.md-tabs{display:none}}.md-header--lifted .md-tabs{box-shadow:none;margin-bottom:-.05rem}.md-tabs[hidden]{pointer-events:none}[dir=ltr] .md-tabs__list{margin-left:.4rem}[dir=rtl] .md-tabs__list{margin-right:.4rem}.md-tabs__list{contain:content;display:flex;list-style:none;margin:0;overflow:auto;padding:0;scrollbar-width:none;white-space:nowrap}.md-tabs__list::-webkit-scrollbar{display:none}.md-tabs__item{height:2.4rem;padding-left:.6rem;padding-right:.6rem}.md-tabs__item--active{border-bottom:.05rem solid var(--md-default-fg-color);font-weight:500;position:relative;transition:border-bottom .25s}.md-tabs[hidden] .md-tabs__item--active{border-bottom:.05rem solid #0000}.md-tabs__item--active .md-tabs__link{color:inherit;opacity:1}.md-tabs__link{backface-visibility:hidden;display:flex;font-size:.7rem;margin-top:.8rem;opacity:.7;outline-color:var(--md-accent-fg-color);outline-offset:.2rem;transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .25s}.md-tabs__link:focus,.md-tabs__link:hover{color:inherit;opacity:1}[dir=ltr] .md-tabs__link svg{margin-right:.4rem}[dir=rtl] .md-tabs__link svg{margin-left:.4rem}.md-tabs__link svg{fill:currentcolor;height:1.3em}.md-tabs__item:nth-child(2) .md-tabs__link{transition-delay:20ms}.md-tabs__item:nth-child(3) .md-tabs__link{transition-delay:40ms}.md-tabs__item:nth-child(4) .md-tabs__link{transition-delay:60ms}.md-tabs__item:nth-child(5) .md-tabs__link{transition-delay:80ms}.md-tabs__item:nth-child(6) .md-tabs__link{transition-delay:.1s}.md-tabs__item:nth-child(7) .md-tabs__link{transition-delay:.12s}.md-tabs__item:nth-child(8) .md-tabs__link{transition-delay:.14s}.md-tabs__item:nth-child(9) .md-tabs__link{transition-delay:.16s}.md-tabs__item:nth-child(10) .md-tabs__link{transition-delay:.18s}.md-tabs__item:nth-child(11) .md-tabs__link{transition-delay:.2s}.md-tabs__item:nth-child(12) .md-tabs__link{transition-delay:.22s}.md-tabs__item:nth-child(13) .md-tabs__link{transition-delay:.24s}.md-tabs__item:nth-child(14) .md-tabs__link{transition-delay:.26s}.md-tabs__item:nth-child(15) .md-tabs__link{transition-delay:.28s}.md-tabs__item:nth-child(16) .md-tabs__link{transition-delay:.3s}.md-tabs[hidden] .md-tabs__link{opacity:0;transform:translateY(50%);transition:transform 0ms .1s,opacity .1s}:root{--md-tag-icon:url('data:image/svg+xml;charset=utf-8, ')}.md-typeset .md-tags:not([hidden]){display:flex;flex-wrap:wrap;gap:.5em;margin-bottom:1.2rem;margin-top:.8rem;padding-top:1.2rem}.md-typeset .md-tag{align-items:center;background:var(--md-default-fg-color--lightest);border-radius:.4rem;display:inline-flex;font-size:.64rem;font-size:min(.8em,.64rem);font-weight:700;gap:.5em;letter-spacing:normal;line-height:1.6;padding:.3125em .78125em}.md-typeset .md-tag[href]{-webkit-tap-highlight-color:transparent;color:inherit;outline:none;transition:color 125ms,background-color 125ms}.md-typeset .md-tag[href]:focus,.md-typeset .md-tag[href]:hover{background-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}[id]>.md-typeset .md-tag{vertical-align:text-top}.md-typeset .md-tag-shadow{opacity:.5}.md-typeset .md-tag-icon:before{background-color:var(--md-default-fg-color--lighter);content:"";display:inline-block;height:1.2em;-webkit-mask-image:var(--md-tag-icon);mask-image:var(--md-tag-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:background-color 125ms;vertical-align:text-bottom;width:1.2em}.md-typeset .md-tag-icon[href]:focus:before,.md-typeset .md-tag-icon[href]:hover:before{background-color:var(--md-accent-bg-color)}@keyframes pulse{0%{transform:scale(.95)}75%{transform:scale(1)}to{transform:scale(.95)}}:root{--md-annotation-bg-icon:url('data:image/svg+xml;charset=utf-8, ');--md-annotation-icon:url('data:image/svg+xml;charset=utf-8, ')}.md-tooltip{backface-visibility:hidden;background-color:var(--md-default-bg-color);border-radius:.4rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color);font-family:var(--md-text-font-family);left:clamp(var(--md-tooltip-0,0rem) + .8rem,var(--md-tooltip-x) - .1rem,100vw + var(--md-tooltip-0,0rem) + .8rem - var(--md-tooltip-width) - 2 * .8rem);max-width:calc(100vw - 1.6rem);opacity:0;position:absolute;top:calc(var(--md-tooltip-y) - .1rem);transform:translateY(-.4rem);transition:transform 0ms .25s,opacity .25s,z-index .25s;width:var(--md-tooltip-width);z-index:0}.md-tooltip--active{opacity:1;transform:translateY(0);transition:transform .25s cubic-bezier(.1,.7,.1,1),opacity .25s,z-index 0ms;z-index:2}.md-tooltip--inline{font-weight:400;-webkit-user-select:none;user-select:none;width:auto}.md-tooltip--inline:not(.md-tooltip--active){transform:translateY(.2rem) scale(.9)}.md-tooltip--inline .md-tooltip__inner{font-size:.55rem;padding:.2rem .4rem}[hidden]+.md-tooltip--inline{display:none}.focus-visible>.md-tooltip,.md-tooltip:target{outline:var(--md-accent-fg-color) auto}.md-tooltip__inner{font-size:.64rem;padding:.8rem}.md-tooltip__inner.md-typeset>:first-child{margin-top:0}.md-tooltip__inner.md-typeset>:last-child{margin-bottom:0}.md-annotation{font-style:normal;font-weight:400;outline:none;text-align:initial;vertical-align:text-bottom;white-space:normal}[dir=rtl] .md-annotation{direction:rtl}code .md-annotation{font-family:var(--md-code-font-family);font-size:inherit}.md-annotation:not([hidden]){display:inline-block;line-height:1.25}.md-annotation__index{border-radius:.01px;cursor:pointer;display:inline-block;margin-left:.4ch;margin-right:.4ch;outline:none;overflow:hidden;position:relative;-webkit-user-select:none;user-select:none;vertical-align:text-top;z-index:0}.md-annotation .md-annotation__index{transition:z-index .25s}@media screen{.md-annotation__index{width:2.2ch}[data-md-visible]>.md-annotation__index{animation:pulse 2s infinite}.md-annotation__index:before{background:var(--md-default-bg-color);-webkit-mask-image:var(--md-annotation-bg-icon);mask-image:var(--md-annotation-bg-icon)}.md-annotation__index:after,.md-annotation__index:before{content:"";height:2.2ch;-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:-.1ch;width:2.2ch;z-index:-1}.md-annotation__index:after{background-color:var(--md-default-fg-color--lighter);-webkit-mask-image:var(--md-annotation-icon);mask-image:var(--md-annotation-icon);transform:scale(1.0001);transition:background-color .25s,transform .25s}.md-tooltip--active+.md-annotation__index:after{transform:rotate(45deg)}.md-tooltip--active+.md-annotation__index:after,:hover>.md-annotation__index:after{background-color:var(--md-accent-fg-color)}}.md-tooltip--active+.md-annotation__index{animation-play-state:paused;transition-duration:0ms;z-index:2}.md-annotation__index [data-md-annotation-id]{display:inline-block}@media print{.md-annotation__index [data-md-annotation-id]{background:var(--md-default-fg-color--lighter);border-radius:2ch;color:var(--md-default-bg-color);font-weight:700;padding:0 .6ch;white-space:nowrap}.md-annotation__index [data-md-annotation-id]:after{content:attr(data-md-annotation-id)}}.md-typeset .md-annotation-list{counter-reset:annotation;list-style:none!important}.md-typeset .md-annotation-list li{position:relative}[dir=ltr] .md-typeset .md-annotation-list li:before{left:-2.125em}[dir=rtl] .md-typeset .md-annotation-list li:before{right:-2.125em}.md-typeset .md-annotation-list li:before{background:var(--md-default-fg-color--lighter);border-radius:2ch;color:var(--md-default-bg-color);content:counter(annotation);counter-increment:annotation;font-size:.8875em;font-weight:700;height:2ch;line-height:1.25;min-width:2ch;padding:0 .6ch;position:absolute;text-align:center;top:.25em}:root{--md-tooltip-width:20rem;--md-tooltip-tail:0.3rem}.md-tooltip2{backface-visibility:hidden;color:var(--md-default-fg-color);font-family:var(--md-text-font-family);opacity:0;pointer-events:none;position:absolute;top:calc(var(--md-tooltip-host-y) + var(--md-tooltip-y));transform:translateY(.4rem);transform-origin:calc(var(--md-tooltip-host-x) + var(--md-tooltip-x)) 0;transition:transform 0ms .25s,opacity .25s,z-index .25s;width:100%;z-index:0}.md-tooltip2:before{border-left:var(--md-tooltip-tail) solid #0000;border-right:var(--md-tooltip-tail) solid #0000;content:"";display:block;left:clamp(1.5 * .8rem,var(--md-tooltip-host-x) + var(--md-tooltip-x) - var(--md-tooltip-tail),100vw - 2 * var(--md-tooltip-tail) - 1.5 * .8rem);position:absolute;z-index:1}.md-tooltip2--top:before{border-top:var(--md-tooltip-tail) solid var(--md-default-bg-color);bottom:calc(var(--md-tooltip-tail)*-1 + .025rem);filter:drop-shadow(0 1px 0 var(--md-default-fg-color--lightest))}.md-tooltip2--bottom:before{border-bottom:var(--md-tooltip-tail) solid var(--md-default-bg-color);filter:drop-shadow(0 -1px 0 var(--md-default-fg-color--lightest));top:calc(var(--md-tooltip-tail)*-1 + .025rem)}.md-tooltip2--active{opacity:1;transform:translateY(0);transition:transform .4s cubic-bezier(0,1,.35,1),opacity .25s,z-index 0ms;z-index:4}.md-tooltip2__inner{scrollbar-gutter:stable;background-color:var(--md-default-bg-color);border-radius:.4rem;box-shadow:var(--md-shadow-z2);left:clamp(.8rem,var(--md-tooltip-host-x) - .8rem,100vw - var(--md-tooltip-width) - .8rem);max-height:40vh;max-width:calc(100vw - 1.6rem);position:relative;scrollbar-width:thin}.md-tooltip2__inner::-webkit-scrollbar{height:.2rem;width:.2rem}.md-tooltip2__inner::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-tooltip2__inner::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}[role=dialog]>.md-tooltip2__inner{font-size:.64rem;overflow:auto;padding:0 .8rem;pointer-events:auto;width:var(--md-tooltip-width)}[role=dialog]>.md-tooltip2__inner:after,[role=dialog]>.md-tooltip2__inner:before{content:"";display:block;height:.8rem;position:sticky;width:100%;z-index:10}[role=dialog]>.md-tooltip2__inner:before{background:linear-gradient(var(--md-default-bg-color),#0000 75%);top:0}[role=dialog]>.md-tooltip2__inner:after{background:linear-gradient(#0000,var(--md-default-bg-color) 75%);bottom:0}[role=tooltip]>.md-tooltip2__inner{font-size:.55rem;font-weight:400;left:clamp(.8rem,var(--md-tooltip-host-x) + var(--md-tooltip-x) - var(--md-tooltip-width)/2,100vw - var(--md-tooltip-width) - .8rem);max-width:min(100vw - 2 * .8rem,400px);padding:.2rem .4rem;-webkit-user-select:none;user-select:none;width:fit-content}.md-tooltip2__inner.md-typeset>:first-child{margin-top:0}.md-tooltip2__inner.md-typeset>:last-child{margin-bottom:0}[dir=ltr] .md-top{margin-left:50%}[dir=rtl] .md-top{margin-right:50%}.md-top{-webkit-backdrop-filter:blur(.4rem);backdrop-filter:blur(.4rem);background-color:var(--md-default-bg-color--light);border-radius:1.6rem;bottom:1.6rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color--light);cursor:pointer;display:flex;font-size:.7rem;gap:.4rem;outline:none;padding:.5rem .9rem .5rem .7rem;position:fixed;top:auto!important;transform:translate(-50%);transition:color 125ms,background-color 125ms,transform 125ms cubic-bezier(.4,0,.2,1),opacity 125ms;z-index:2}@media print{.md-top{display:none}}[dir=rtl] .md-top{transform:translate(50%)}.md-top[hidden]{opacity:0;pointer-events:none;transform:translate(-50%,.2rem);transition-duration:0ms}[dir=rtl] .md-top[hidden]{transform:translate(50%,.2rem)}.md-top:focus,.md-top:hover{background-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}.md-top svg{display:inline-block;height:.9rem;vertical-align:-.5em;width:.9rem}.md-top svg.lucide{fill:#0000;stroke:currentcolor}@keyframes hoverfix{0%{pointer-events:none}}:root{--md-version-icon:url('data:image/svg+xml;charset=utf-8, ')}.md-version{flex-shrink:0;font-size:.8rem;height:2.4rem}[dir=ltr] .md-version__current{margin-left:1.4rem;margin-right:.4rem}[dir=rtl] .md-version__current{margin-left:.4rem;margin-right:1.4rem}.md-version__current{color:inherit;cursor:pointer;outline:none;position:relative;top:.05rem}[dir=ltr] .md-version__current:after{margin-left:.4rem}[dir=rtl] .md-version__current:after{margin-right:.4rem}.md-version__current:after{background-color:currentcolor;content:"";display:inline-block;height:.6rem;-webkit-mask-image:var(--md-version-icon);mask-image:var(--md-version-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:.4rem}.md-version__alias{margin-left:.3rem;opacity:.7}.md-version__list{background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color);list-style-type:none;margin:.2rem .8rem;max-height:0;opacity:0;overflow:auto;padding:0;position:absolute;scroll-snap-type:y mandatory;top:.15rem;transition:max-height 0ms .5s,opacity .25s .25s;z-index:3}.md-version:focus-within .md-version__list,.md-version:hover .md-version__list{max-height:10rem;opacity:1;transition:max-height 0ms,opacity .25s}@media (hover:none),(pointer:coarse){.md-version:hover .md-version__list{animation:hoverfix .25s forwards}.md-version:focus-within .md-version__list{animation:none}}.md-version__item{line-height:1.8rem}[dir=ltr] .md-version__link{padding-left:.6rem;padding-right:1.2rem}[dir=rtl] .md-version__link{padding-left:1.2rem;padding-right:.6rem}.md-version__link{cursor:pointer;display:block;outline:none;scroll-snap-align:start;transition:color .25s,background-color .25s;white-space:nowrap;width:100%}.md-version__link:focus,.md-version__link:hover{color:var(--md-accent-fg-color)}.md-version__link:focus{background-color:var(--md-default-fg-color--lightest)}:root{--md-admonition-icon--note:url('data:image/svg+xml;charset=utf-8, ');--md-admonition-icon--abstract:url('data:image/svg+xml;charset=utf-8, ');--md-admonition-icon--info:url('data:image/svg+xml;charset=utf-8, ');--md-admonition-icon--tip:url('data:image/svg+xml;charset=utf-8, ');--md-admonition-icon--success:url('data:image/svg+xml;charset=utf-8, ');--md-admonition-icon--question:url('data:image/svg+xml;charset=utf-8, ');--md-admonition-icon--warning:url('data:image/svg+xml;charset=utf-8, ');--md-admonition-icon--failure:url('data:image/svg+xml;charset=utf-8, ');--md-admonition-icon--danger:url('data:image/svg+xml;charset=utf-8, ');--md-admonition-icon--bug:url('data:image/svg+xml;charset=utf-8, ');--md-admonition-icon--example:url('data:image/svg+xml;charset=utf-8, ');--md-admonition-icon--quote:url('data:image/svg+xml;charset=utf-8, ')}.md-typeset .admonition,.md-typeset details{background-color:#448aff1a;border-radius:.4rem;color:var(--md-admonition-fg-color);display:flow-root;font-size:.64rem;margin:1.5625em 0;padding:0 .8rem;page-break-inside:avoid}.md-typeset .admonition>*,.md-typeset details>*{box-sizing:border-box}.md-typeset .admonition .admonition,.md-typeset .admonition details,.md-typeset details .admonition,.md-typeset details details{margin-bottom:1em;margin-top:1em}.md-typeset .admonition .md-typeset__scrollwrap,.md-typeset details .md-typeset__scrollwrap{margin:1em -.6rem}.md-typeset .admonition .md-typeset__table,.md-typeset details .md-typeset__table{padding:0 .6rem}.md-typeset .admonition>.tabbed-set:only-child,.md-typeset details>.tabbed-set:only-child{margin-top:0}html .md-typeset .admonition>:last-child,html .md-typeset details>:last-child{margin-bottom:.6rem}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary{padding-left:1.6rem;padding-right:.8rem}[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{padding-left:.8rem;padding-right:1.6rem}.md-typeset .admonition-title,.md-typeset summary{font-weight:700;margin-bottom:1em;margin-top:.6rem;position:relative}[dir=ltr] .md-typeset .admonition-title:before,[dir=ltr] .md-typeset summary:before{left:0}[dir=rtl] .md-typeset .admonition-title:before,[dir=rtl] .md-typeset summary:before{right:0}.md-typeset .admonition-title:before,.md-typeset summary:before{background-color:#448aff;content:"";height:1rem;-webkit-mask-image:var(--md-admonition-icon--note);mask-image:var(--md-admonition-icon--note);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.125em;width:1rem}.md-typeset .admonition.note,.md-typeset details.note{background-color:#448aff1a}.md-typeset .note>.admonition-title:before,.md-typeset .note>summary:before{background-color:#448aff;-webkit-mask-image:var(--md-admonition-icon--note);mask-image:var(--md-admonition-icon--note)}.md-typeset .note>.admonition-title:after,.md-typeset .note>summary:after{color:#448aff}.md-typeset .admonition.abstract,.md-typeset details.abstract{background-color:#00b0ff1a}.md-typeset .abstract>.admonition-title:before,.md-typeset .abstract>summary:before{background-color:#00b0ff;-webkit-mask-image:var(--md-admonition-icon--abstract);mask-image:var(--md-admonition-icon--abstract)}.md-typeset .abstract>.admonition-title:after,.md-typeset .abstract>summary:after{color:#00b0ff}.md-typeset .admonition.info,.md-typeset details.info{background-color:#00b8d41a}.md-typeset .info>.admonition-title:before,.md-typeset .info>summary:before{background-color:#00b8d4;-webkit-mask-image:var(--md-admonition-icon--info);mask-image:var(--md-admonition-icon--info)}.md-typeset .info>.admonition-title:after,.md-typeset .info>summary:after{color:#00b8d4}.md-typeset .admonition.tip,.md-typeset details.tip{background-color:#00bfa51a}.md-typeset .tip>.admonition-title:before,.md-typeset .tip>summary:before{background-color:#00bfa5;-webkit-mask-image:var(--md-admonition-icon--tip);mask-image:var(--md-admonition-icon--tip)}.md-typeset .tip>.admonition-title:after,.md-typeset .tip>summary:after{color:#00bfa5}.md-typeset .admonition.success,.md-typeset details.success{background-color:#00c8531a}.md-typeset .success>.admonition-title:before,.md-typeset .success>summary:before{background-color:#00c853;-webkit-mask-image:var(--md-admonition-icon--success);mask-image:var(--md-admonition-icon--success)}.md-typeset .success>.admonition-title:after,.md-typeset .success>summary:after{color:#00c853}.md-typeset .admonition.question,.md-typeset details.question{background-color:#64dd171a}.md-typeset .question>.admonition-title:before,.md-typeset .question>summary:before{background-color:#64dd17;-webkit-mask-image:var(--md-admonition-icon--question);mask-image:var(--md-admonition-icon--question)}.md-typeset .question>.admonition-title:after,.md-typeset .question>summary:after{color:#64dd17}.md-typeset .admonition.warning,.md-typeset details.warning{background-color:#ff91001a}.md-typeset .warning>.admonition-title:before,.md-typeset .warning>summary:before{background-color:#ff9100;-webkit-mask-image:var(--md-admonition-icon--warning);mask-image:var(--md-admonition-icon--warning)}.md-typeset .warning>.admonition-title:after,.md-typeset .warning>summary:after{color:#ff9100}.md-typeset .admonition.failure,.md-typeset details.failure{background-color:#ff52521a}.md-typeset .failure>.admonition-title:before,.md-typeset .failure>summary:before{background-color:#ff5252;-webkit-mask-image:var(--md-admonition-icon--failure);mask-image:var(--md-admonition-icon--failure)}.md-typeset .failure>.admonition-title:after,.md-typeset .failure>summary:after{color:#ff5252}.md-typeset .admonition.danger,.md-typeset details.danger{background-color:#ff17441a}.md-typeset .danger>.admonition-title:before,.md-typeset .danger>summary:before{background-color:#ff1744;-webkit-mask-image:var(--md-admonition-icon--danger);mask-image:var(--md-admonition-icon--danger)}.md-typeset .danger>.admonition-title:after,.md-typeset .danger>summary:after{color:#ff1744}.md-typeset .admonition.bug,.md-typeset details.bug{background-color:#f500571a}.md-typeset .bug>.admonition-title:before,.md-typeset .bug>summary:before{background-color:#f50057;-webkit-mask-image:var(--md-admonition-icon--bug);mask-image:var(--md-admonition-icon--bug)}.md-typeset .bug>.admonition-title:after,.md-typeset .bug>summary:after{color:#f50057}.md-typeset .admonition.example,.md-typeset details.example{background-color:#7c4dff1a}.md-typeset .example>.admonition-title:before,.md-typeset .example>summary:before{background-color:#7c4dff;-webkit-mask-image:var(--md-admonition-icon--example);mask-image:var(--md-admonition-icon--example)}.md-typeset .example>.admonition-title:after,.md-typeset .example>summary:after{color:#7c4dff}.md-typeset .admonition.quote,.md-typeset details.quote{background-color:#9e9e9e1a}.md-typeset .quote>.admonition-title:before,.md-typeset .quote>summary:before{background-color:#9e9e9e;-webkit-mask-image:var(--md-admonition-icon--quote);mask-image:var(--md-admonition-icon--quote)}.md-typeset .quote>.admonition-title:after,.md-typeset .quote>summary:after{color:#9e9e9e}:root{--md-footnotes-icon:url('data:image/svg+xml;charset=utf-8, ')}.md-typeset .footnote{color:var(--md-default-fg-color--light);font-size:.64rem}[dir=ltr] .md-typeset .footnote>ol{margin-left:0}[dir=rtl] .md-typeset .footnote>ol{margin-right:0}.md-typeset .footnote>ol>li{transition:color 125ms}.md-typeset .footnote>ol>li:target{color:var(--md-default-fg-color)}.md-typeset .footnote>ol>li:focus-within .footnote-backref{opacity:1;transform:translateY(0);transition:none}.md-typeset .footnote>ol>li:hover .footnote-backref,.md-typeset .footnote>ol>li:target .footnote-backref{opacity:1;transform:translateY(0)}.md-typeset .footnote>ol>li>:first-child{margin-top:0}.md-typeset .footnote-ref{font-size:.75em;font-weight:700;text-decoration:none}html .md-typeset .footnote-ref{outline-offset:.1rem}.md-typeset [id^="fnref:"]:target>.footnote-ref{outline:auto}.md-typeset .footnote-backref{color:var(--md-typeset-a-color);display:inline-block;font-size:0;opacity:0;transform:translateY(.25rem);transition:color .25s,transform .25s .25s,opacity 125ms .25s;vertical-align:text-bottom}@media print{.md-typeset .footnote-backref{color:var(--md-typeset-a-color);opacity:1;transform:translateY(0)}}[dir=rtl] .md-typeset .footnote-backref{transform:translateY(-.25rem)}.md-typeset .footnote-backref:hover{color:var(--md-accent-fg-color)}.md-typeset .footnote-backref:before{background-color:currentcolor;content:"";display:inline-block;height:.8rem;-webkit-mask-image:var(--md-footnotes-icon);mask-image:var(--md-footnotes-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:.8rem}[dir=rtl] .md-typeset .footnote-backref:before{transform:scaleX(-1)}[dir=ltr] .md-typeset .headerlink{margin-left:.5rem}[dir=rtl] .md-typeset .headerlink{margin-right:.5rem}.md-typeset .headerlink{color:var(--md-default-fg-color--lighter);display:inline-block;opacity:0;text-decoration:none;transition:color .25s,opacity 125ms}@media print{.md-typeset .headerlink{display:none}}.md-typeset .headerlink:focus,.md-typeset :hover>.headerlink,.md-typeset :target>.headerlink{opacity:1;transition:color .25s,opacity 125ms}.md-typeset .headerlink:focus,.md-typeset .headerlink:hover,.md-typeset :target>.headerlink{color:var(--md-accent-fg-color)}.md-typeset :target{--md-scroll-margin:3.6rem;--md-scroll-offset:0rem;scroll-margin-top:calc(var(--md-scroll-margin) - var(--md-scroll-offset))}@media screen and (min-width:76.25em){.md-header--lifted~.md-container .md-typeset :target{--md-scroll-margin:6rem}}.md-typeset h1:target{--md-scroll-offset:0.1rem}.md-typeset h3:target,.md-typeset h4:target{--md-scroll-offset:-0.1rem}:root{--md-admonition-icon--mkdocstrings:url('data:image/svg+xml;charset=utf-8, ');--md-admonition-icon--mkdocstrings-open:url('data:image/svg+xml;charset=utf-8, ')}.doc-object-name{font-family:var(--md-code-font-family)}code.doc-symbol-heading{margin-right:.4rem;padding:0}[dir=ltr] .doc-labels{margin-left:.4rem}[dir=rtl] .doc-labels{margin-right:.4rem}.doc-label code{background:#0000;border:1px solid var(--md-default-fg-color--lightest);border-radius:.5rem;color:var(--md-default-fg-color--light);font-weight:400;padding-left:.3rem;padding-right:.3rem;vertical-align:text-bottom}.doc-contents td code{word-break:normal!important}.doc-md-description,.doc-md-description>p:first-child{display:inline}.md-typeset h5 .doc-object-name{text-transform:none}.doc .md-typeset__table,.doc .md-typeset__table table{display:table!important;width:100%}.doc .md-typeset__table tr{display:table-row}.doc-param-default,.doc-type_param-default{float:right}.doc-heading-parameter,.doc-heading-type_parameter{display:inline}.md-typeset .doc-heading-parameter{font-size:inherit}.doc-heading-parameter .headerlink,.doc-heading-type_parameter .headerlink{margin-left:0!important;margin-right:.2rem}.doc-section-title{font-weight:700}.doc-signature .autorefs{color:inherit;text-decoration-style:dotted}div.doc-contents:not(.first){border-left:.05rem solid var(--md-code-bg-color);margin-left:.4rem;padding-left:.8rem}:host,:root,[data-md-color-scheme=default]{--doc-symbol-parameter-fg-color:#829bd1;--doc-symbol-type_parameter-fg-color:#829bd1;--doc-symbol-attribute-fg-color:#953800;--doc-symbol-function-fg-color:#8250df;--doc-symbol-method-fg-color:#8250df;--doc-symbol-class-fg-color:#0550ae;--doc-symbol-type_alias-fg-color:#0550ae;--doc-symbol-module-fg-color:#5cad0f}[data-md-color-scheme=slate]{--doc-symbol-parameter-fg-color:#829bd1;--doc-symbol-type_parameter-fg-color:#829bd1;--doc-symbol-attribute-fg-color:#ffa657;--doc-symbol-function-fg-color:#d2a8ff;--doc-symbol-method-fg-color:#d2a8ff;--doc-symbol-class-fg-color:#79c0ff;--doc-symbol-type_alias-fg-color:#79c0ff;--doc-symbol-module-fg-color:#baff79}.md-ellipsis:has(.doc-symbol){font-family:var(--md-code-font-family);font-size:.95em}code.doc-symbol{background-color:initial;border-radius:.1rem;font-size:1em;font-weight:400}a code.doc-symbol-parameter,code.doc-symbol-parameter{color:var(--doc-symbol-parameter-fg-color)}.md-content code.doc-symbol-parameter:after{content:"param"}.md-sidebar code.doc-symbol-parameter:after{content:"p"}a code.doc-symbol-type_parameter,code.doc-symbol-type_parameter{color:var(--doc-symbol-type_parameter-fg-color)}.md-content code.doc-symbol-type_parameter:after{content:"type-param"}.md-sidebar code.doc-symbol-type_parameter:after{content:"t"}a code.doc-symbol-attribute,code.doc-symbol-attribute{color:var(--doc-symbol-attribute-fg-color)}.md-content code.doc-symbol-attribute:after{content:"attribute"}.md-sidebar code.doc-symbol-attribute:after{content:"a"}a code.doc-symbol-function,code.doc-symbol-function{color:var(--doc-symbol-function-fg-color)}.md-content code.doc-symbol-function:after{content:"function"}.md-sidebar code.doc-symbol-function:after{content:"f"}a code.doc-symbol-method,code.doc-symbol-method{color:var(--doc-symbol-method-fg-color)}.md-content code.doc-symbol-method:after{content:"method"}.md-sidebar code.doc-symbol-method:after{content:"m"}a code.doc-symbol-class,code.doc-symbol-class{color:var(--doc-symbol-class-fg-color)}.md-content code.doc-symbol-class:after{content:"class"}.md-sidebar code.doc-symbol-class:after{content:"c"}a code.doc-symbol-type_alias,code.doc-symbol-type_alias{color:var(--doc-symbol-type_alias-fg-color)}.md-content code.doc-symbol-type_alias:after{content:"type"}.md-sidebar code.doc-symbol-type_alias:after{content:"t"}a code.doc-symbol-module,code.doc-symbol-module{color:var(--doc-symbol-module-fg-color)}.md-content code.doc-symbol-module:after{content:"module"}.md-sidebar code.doc-symbol-module:after{content:"mod"}.md-typeset details.mkdocstrings-source{background:#0000;border:.05rem solid var(--md-code-bg-color)}.md-typeset details.mkdocstrings-source>summary:before{background-color:var(--md-default-fg-color--light);-webkit-mask-image:var(--md-admonition-icon--mkdocstrings);mask-image:var(--md-admonition-icon--mkdocstrings)}.md-typeset details.mkdocstrings-source[open]>summary:before{-webkit-mask-image:var(--md-admonition-icon--mkdocstrings-open);mask-image:var(--md-admonition-icon--mkdocstrings-open)}.md-typeset details.mkdocstrings-source>summary:after{background-color:var(--md-default-fg-color--light)}.md-typeset div.arithmatex{overflow:auto}@media screen and (max-width:44.984375em){.md-typeset div.arithmatex{margin:0 -.8rem}.md-typeset div.arithmatex>*{width:min-content}}.md-typeset div.arithmatex>*{margin-left:auto!important;margin-right:auto!important;padding:0 .8rem;touch-action:auto}.md-typeset div.arithmatex>* mjx-container{margin:0!important}.md-typeset div.arithmatex mjx-assistive-mml{height:0}.md-typeset del.critic{background-color:var(--md-typeset-del-color)}.md-typeset del.critic,.md-typeset ins.critic{-webkit-box-decoration-break:clone;box-decoration-break:clone}.md-typeset ins.critic{background-color:var(--md-typeset-ins-color)}.md-typeset .critic.comment{-webkit-box-decoration-break:clone;box-decoration-break:clone;color:var(--md-code-hl-comment-color)}.md-typeset .critic.comment:before{content:"/* "}.md-typeset .critic.comment:after{content:" */"}.md-typeset .critic.block{box-shadow:none;display:block;margin:1em 0;overflow:auto;padding-left:.8rem;padding-right:.8rem}.md-typeset .critic.block>:first-child{margin-top:.5em}.md-typeset .critic.block>:last-child{margin-bottom:.5em}:root{--md-details-icon:url('data:image/svg+xml;charset=utf-8, ')}.md-typeset details{display:flow-root;overflow:visible;padding-top:0}.md-typeset details[open]>summary:after{transform:rotate(90deg)}.md-typeset details:not([open]){box-shadow:none;padding-bottom:0}.md-typeset details:not([open])>summary{border-radius:.1rem;margin-bottom:.6rem}[dir=ltr] .md-typeset summary{padding-right:1.6rem}[dir=rtl] .md-typeset summary{padding-left:1.6rem}[dir=ltr] .md-typeset summary{border-top-left-radius:.1rem}[dir=ltr] .md-typeset summary,[dir=rtl] .md-typeset summary{border-top-right-radius:.1rem}[dir=rtl] .md-typeset summary{border-top-left-radius:.1rem}.md-typeset summary{cursor:pointer;display:block;min-height:1rem;overflow:hidden}.md-typeset summary.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-typeset summary:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}[dir=ltr] .md-typeset summary:after{right:0}[dir=rtl] .md-typeset summary:after{left:0}.md-typeset summary:after{background-color:currentcolor;content:"";height:1rem;-webkit-mask-image:var(--md-details-icon);mask-image:var(--md-details-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.125em;transform:rotate(0deg);transition:transform .25s;width:1rem}[dir=rtl] .md-typeset summary:after{transform:rotate(180deg)}.md-typeset summary::marker{display:none}.md-typeset summary::-webkit-details-marker{display:none}.md-typeset .emojione,.md-typeset .gemoji,.md-typeset .twemoji{--md-icon-size:1.125em;display:inline-flex;height:var(--md-icon-size);vertical-align:text-top}.md-typeset .emojione svg,.md-typeset .gemoji svg,.md-typeset .twemoji svg{fill:currentcolor;max-height:100%;width:var(--md-icon-size)}.md-typeset .emojione svg.lucide,.md-typeset .gemoji svg.lucide,.md-typeset .twemoji svg.lucide{fill:#0000;stroke:currentcolor}.md-typeset .lg,.md-typeset .xl,.md-typeset .xxl,.md-typeset .xxxl{vertical-align:text-bottom}.md-typeset .middle{vertical-align:middle}.md-typeset .lg{--md-icon-size:1.5em}.md-typeset .xl{--md-icon-size:2.25em}.md-typeset .xxl{--md-icon-size:3em}.md-typeset .xxxl{--md-icon-size:4em}.highlight .o,.highlight .ow{color:var(--md-code-hl-operator-color)}.highlight .p{color:var(--md-code-hl-punctuation-color)}.highlight .cpf,.highlight .l,.highlight .s,.highlight .s1,.highlight .s2,.highlight .sb,.highlight .sc,.highlight .si,.highlight .ss{color:var(--md-code-hl-string-color)}.highlight .cp,.highlight .se,.highlight .sh,.highlight .sr,.highlight .sx{color:var(--md-code-hl-special-color)}.highlight .il,.highlight .m,.highlight .mb,.highlight .mf,.highlight .mh,.highlight .mi,.highlight .mo{color:var(--md-code-hl-number-color)}.highlight .k,.highlight .kd,.highlight .kn,.highlight .kp,.highlight .kr,.highlight .kt{color:var(--md-code-hl-keyword-color)}.highlight .kc,.highlight .n{color:var(--md-code-hl-name-color)}.highlight .bp,.highlight .nb,.highlight .no{color:var(--md-code-hl-constant-color)}.highlight .nc,.highlight .ne,.highlight .nf,.highlight .nn{color:var(--md-code-hl-function-color)}.highlight .nd,.highlight .ni,.highlight .nl,.highlight .nt{color:var(--md-code-hl-keyword-color)}.highlight .c,.highlight .c1,.highlight .ch,.highlight .cm,.highlight .cs,.highlight .sd{color:var(--md-code-hl-comment-color)}.highlight .na,.highlight .nv,.highlight .vc,.highlight .vg,.highlight .vi{color:var(--md-code-hl-variable-color)}.highlight .ge,.highlight .gh,.highlight .go,.highlight .gp,.highlight .gr,.highlight .gs,.highlight .gt,.highlight .gu{color:var(--md-code-hl-generic-color)}.highlight .gd,.highlight .gi{border-radius:.1rem;margin:0 -.125em;padding:0 .125em}.highlight .gd{background-color:var(--md-typeset-del-color)}.highlight .gi{background-color:var(--md-typeset-ins-color)}.highlight .hll{background-color:var(--md-code-hl-color--light);box-shadow:2px 0 0 0 var(--md-code-hl-color) inset;display:block;margin:0 -1.1764705882em;padding:0 1.1764705882em}.highlight span.filename{background-color:var(--md-code-bg-color);border-bottom:.05rem solid var(--md-default-fg-color--lightest);border-top-left-radius:.4rem;border-top-right-radius:.4rem;display:flow-root;font-size:.85em;font-weight:700;margin-top:1em;padding:.6617647059em 1.1764705882em;position:relative}.highlight span.filename+pre{margin-top:0}.highlight span.filename+pre>code{border-top-left-radius:0;border-top-right-radius:0}.highlight [data-linenos]:before{background-color:var(--md-code-bg-color);box-shadow:-.05rem 0 var(--md-default-fg-color--lightest) inset;color:var(--md-default-fg-color--light);content:attr(data-linenos);float:left;left:-1.1764705882em;margin-left:-1.1764705882em;margin-right:1.1764705882em;padding-left:1.1764705882em;position:sticky;-webkit-user-select:none;user-select:none;z-index:3}.highlight code>span[id^=__span]>:last-child .md-annotation{margin-right:2.4rem}.highlight code[data-md-copying]{display:initial}.highlight code[data-md-copying] .hll{display:contents}.highlight code[data-md-copying] .md-annotation{display:none}.highlighttable{display:flow-root}.highlighttable tbody,.highlighttable td{display:block;padding:0}.highlighttable tr{display:flex}.highlighttable pre{margin:0}.highlighttable th.filename{flex-grow:1;padding:0;text-align:left}.highlighttable th.filename span.filename{margin-top:0}.highlighttable .linenos{background-color:var(--md-code-bg-color);border-bottom-left-radius:.4rem;border-top-left-radius:.4rem;font-size:.85em;padding:.7720588235em 0 .7720588235em 1.1764705882em;-webkit-user-select:none;user-select:none}.highlighttable .linenodiv{box-shadow:-.05rem 0 var(--md-default-fg-color--lightest) inset}.highlighttable .linenodiv pre{color:var(--md-default-fg-color--light);text-align:right}.highlighttable .linenodiv span[class]{padding-right:.5882352941em}.highlighttable .code{flex:1;min-width:0}.linenodiv a{color:inherit;text-decoration:none}.md-typeset .highlighttable{direction:ltr;margin:1em 0}.md-typeset .highlighttable>tbody>tr>.code>div>pre>code{border-bottom-left-radius:0;border-top-left-radius:0}.md-typeset .highlight+.result{border:.05rem solid var(--md-code-bg-color);border-bottom-left-radius:.4rem;border-bottom-right-radius:.4rem;border-top-width:.4rem;margin-top:-1.5em;overflow:visible;padding:0 1em}.md-typeset .highlight+.result:after{clear:both;content:"";display:block}@media screen and (max-width:44.984375em){.md-content__inner>.highlight{margin:1em -.8rem}.md-content__inner>.highlight>.filename,.md-content__inner>.highlight>.highlighttable>tbody>tr>.code>div>pre>code,.md-content__inner>.highlight>.highlighttable>tbody>tr>.filename span.filename,.md-content__inner>.highlight>.highlighttable>tbody>tr>.linenos,.md-content__inner>.highlight>pre>code{border-radius:0}.md-content__inner>.highlight+.result{border-left-width:0;border-radius:0;border-right-width:0;margin-left:-.8rem;margin-right:-.8rem}}.md-typeset .keys kbd:after,.md-typeset .keys kbd:before{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;color:inherit;margin:0;position:relative}.md-typeset .keys span{color:var(--md-default-fg-color--light);padding:0 .2em}.md-typeset .keys .key-alt:before,.md-typeset .keys .key-left-alt:before,.md-typeset .keys .key-right-alt:before{content:"⎇";padding-right:.4em}.md-typeset .keys .key-command:before,.md-typeset .keys .key-left-command:before,.md-typeset .keys .key-right-command:before{content:"⌘";padding-right:.4em}.md-typeset .keys .key-control:before,.md-typeset .keys .key-left-control:before,.md-typeset .keys .key-right-control:before{content:"⌃";padding-right:.4em}.md-typeset .keys .key-left-meta:before,.md-typeset .keys .key-meta:before,.md-typeset .keys .key-right-meta:before{content:"◆";padding-right:.4em}.md-typeset .keys .key-left-option:before,.md-typeset .keys .key-option:before,.md-typeset .keys .key-right-option:before{content:"⌥";padding-right:.4em}.md-typeset .keys .key-left-shift:before,.md-typeset .keys .key-right-shift:before,.md-typeset .keys .key-shift:before{content:"⇧";padding-right:.4em}.md-typeset .keys .key-left-super:before,.md-typeset .keys .key-right-super:before,.md-typeset .keys .key-super:before{content:"❖";padding-right:.4em}.md-typeset .keys .key-left-windows:before,.md-typeset .keys .key-right-windows:before,.md-typeset .keys .key-windows:before{content:"⊞";padding-right:.4em}.md-typeset .keys .key-arrow-down:before{content:"↓";padding-right:.4em}.md-typeset .keys .key-arrow-left:before{content:"←";padding-right:.4em}.md-typeset .keys .key-arrow-right:before{content:"→";padding-right:.4em}.md-typeset .keys .key-arrow-up:before{content:"↑";padding-right:.4em}.md-typeset .keys .key-backspace:before{content:"⌫";padding-right:.4em}.md-typeset .keys .key-backtab:before{content:"⇤";padding-right:.4em}.md-typeset .keys .key-caps-lock:before{content:"⇪";padding-right:.4em}.md-typeset .keys .key-clear:before{content:"⌧";padding-right:.4em}.md-typeset .keys .key-context-menu:before{content:"☰";padding-right:.4em}.md-typeset .keys .key-delete:before{content:"⌦";padding-right:.4em}.md-typeset .keys .key-eject:before{content:"⏏";padding-right:.4em}.md-typeset .keys .key-end:before{content:"⤓";padding-right:.4em}.md-typeset .keys .key-escape:before{content:"⎋";padding-right:.4em}.md-typeset .keys .key-home:before{content:"⤒";padding-right:.4em}.md-typeset .keys .key-insert:before{content:"⎀";padding-right:.4em}.md-typeset .keys .key-page-down:before{content:"⇟";padding-right:.4em}.md-typeset .keys .key-page-up:before{content:"⇞";padding-right:.4em}.md-typeset .keys .key-print-screen:before{content:"⎙";padding-right:.4em}.md-typeset .keys .key-tab:after{content:"⇥";padding-left:.4em}.md-typeset .keys .key-num-enter:after{content:"⌤";padding-left:.4em}.md-typeset .keys .key-enter:after{content:"⏎";padding-left:.4em}:root{--md-tabbed-icon--prev:url('data:image/svg+xml;charset=utf-8, ');--md-tabbed-icon--next:url('data:image/svg+xml;charset=utf-8, ')}.md-typeset .tabbed-set{border-radius:.075rem;display:flex;flex-flow:column wrap;margin:1em 0;position:relative}.md-typeset .tabbed-set>input{height:0;opacity:0;position:absolute;width:0}.md-typeset .tabbed-set>input:target{--md-scroll-offset:0.625em}.md-typeset .tabbed-set>input.focus-visible~.tabbed-labels:before{background-color:var(--md-accent-fg-color)}.md-typeset .tabbed-labels{-ms-overflow-style:none;box-shadow:0 -.05rem var(--md-default-fg-color--lightest) inset;display:flex;max-width:100%;overflow:auto;scrollbar-width:none}@media print{.md-typeset .tabbed-labels{display:contents}}@media screen{.js .md-typeset .tabbed-labels{position:relative}.js .md-typeset .tabbed-labels:before{background:var(--md-default-fg-color);bottom:0;content:"";display:block;height:1.5px;left:0;position:absolute;transform:translateX(var(--md-indicator-x));transition:width 225ms,background-color .25s,transform .25s;transition-timing-function:cubic-bezier(.4,0,.2,1);width:var(--md-indicator-width)}}.md-typeset .tabbed-labels::-webkit-scrollbar{display:none}.md-typeset .tabbed-labels>label{border-bottom:.1rem solid #0000;border-radius:.1rem .1rem 0 0;color:var(--md-default-fg-color--light);cursor:pointer;flex-shrink:0;font-size:.7rem;font-weight:400;padding:.78125em 1.25em .625em;scroll-margin-inline-start:1rem;transition:background-color .25s,color .25s;white-space:nowrap;width:auto}@media print{.md-typeset .tabbed-labels>label:first-child{order:1}.md-typeset .tabbed-labels>label:nth-child(2){order:2}.md-typeset .tabbed-labels>label:nth-child(3){order:3}.md-typeset .tabbed-labels>label:nth-child(4){order:4}.md-typeset .tabbed-labels>label:nth-child(5){order:5}.md-typeset .tabbed-labels>label:nth-child(6){order:6}.md-typeset .tabbed-labels>label:nth-child(7){order:7}.md-typeset .tabbed-labels>label:nth-child(8){order:8}.md-typeset .tabbed-labels>label:nth-child(9){order:9}.md-typeset .tabbed-labels>label:nth-child(10){order:10}.md-typeset .tabbed-labels>label:nth-child(11){order:11}.md-typeset .tabbed-labels>label:nth-child(12){order:12}.md-typeset .tabbed-labels>label:nth-child(13){order:13}.md-typeset .tabbed-labels>label:nth-child(14){order:14}.md-typeset .tabbed-labels>label:nth-child(15){order:15}.md-typeset .tabbed-labels>label:nth-child(16){order:16}.md-typeset .tabbed-labels>label:nth-child(17){order:17}.md-typeset .tabbed-labels>label:nth-child(18){order:18}.md-typeset .tabbed-labels>label:nth-child(19){order:19}.md-typeset .tabbed-labels>label:nth-child(20){order:20}}.md-typeset .tabbed-labels>label:hover{color:var(--md-default-fg-color)}.md-typeset .tabbed-labels>label>[href]:first-child{color:inherit;text-decoration:none}.md-typeset .tabbed-labels--linked>label{padding:0}.md-typeset .tabbed-labels--linked>label>a{display:block;padding:.78125em 1.25em .625em}.md-typeset .tabbed-content{width:100%}@media print{.md-typeset .tabbed-content{display:contents}}.md-typeset .tabbed-block{display:none}@media print{.md-typeset .tabbed-block{display:block}.md-typeset .tabbed-block:first-child{order:1}.md-typeset .tabbed-block:nth-child(2){order:2}.md-typeset .tabbed-block:nth-child(3){order:3}.md-typeset .tabbed-block:nth-child(4){order:4}.md-typeset .tabbed-block:nth-child(5){order:5}.md-typeset .tabbed-block:nth-child(6){order:6}.md-typeset .tabbed-block:nth-child(7){order:7}.md-typeset .tabbed-block:nth-child(8){order:8}.md-typeset .tabbed-block:nth-child(9){order:9}.md-typeset .tabbed-block:nth-child(10){order:10}.md-typeset .tabbed-block:nth-child(11){order:11}.md-typeset .tabbed-block:nth-child(12){order:12}.md-typeset .tabbed-block:nth-child(13){order:13}.md-typeset .tabbed-block:nth-child(14){order:14}.md-typeset .tabbed-block:nth-child(15){order:15}.md-typeset .tabbed-block:nth-child(16){order:16}.md-typeset .tabbed-block:nth-child(17){order:17}.md-typeset .tabbed-block:nth-child(18){order:18}.md-typeset .tabbed-block:nth-child(19){order:19}.md-typeset .tabbed-block:nth-child(20){order:20}}.md-typeset .tabbed-block>.highlight:first-child>pre,.md-typeset .tabbed-block>pre:first-child{margin:0}.md-typeset .tabbed-block>.highlight:first-child>pre>code,.md-typeset .tabbed-block>pre:first-child>code{border-top-left-radius:0;border-top-right-radius:0}.md-typeset .tabbed-block>.highlight:first-child>.filename{border-top-left-radius:0;border-top-right-radius:0;margin:0}.md-typeset .tabbed-block>.highlight:first-child>.highlighttable{margin:0}.md-typeset .tabbed-block>.highlight:first-child>.highlighttable>tbody>tr>.filename span.filename,.md-typeset .tabbed-block>.highlight:first-child>.highlighttable>tbody>tr>.linenos{border-top-left-radius:0;border-top-right-radius:0;margin:0}.md-typeset .tabbed-block>.highlight:first-child>.highlighttable>tbody>tr>.code>div>pre>code{border-top-left-radius:0;border-top-right-radius:0}.md-typeset .tabbed-block>.highlight:first-child+.result{margin-top:-.125em}.md-typeset .tabbed-block>.tabbed-set{margin:0}.md-typeset .tabbed-button{align-self:center;-webkit-backdrop-filter:blur(.4rem);backdrop-filter:blur(.4rem);background-color:var(--md-default-bg-color--light);border-radius:100%;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color--light);cursor:pointer;display:block;height:.9rem;margin-top:.4rem;pointer-events:auto;transition:transform 125ms;width:.9rem}.md-typeset .tabbed-button:hover{transform:scale(1.125)}.md-typeset .tabbed-button:after{background-color:currentcolor;content:"";display:block;height:100%;-webkit-mask-image:var(--md-tabbed-icon--prev);mask-image:var(--md-tabbed-icon--prev);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:background-color .25s,transform .25s;width:100%}.md-typeset .tabbed-control{display:flex;height:1.9rem;justify-content:start;pointer-events:none;position:absolute;transition:opacity 125ms;width:1.2rem}[dir=rtl] .md-typeset .tabbed-control{transform:rotate(180deg)}.md-typeset .tabbed-control[hidden]{opacity:0}.md-typeset .tabbed-control--next{justify-content:end;right:0}.md-typeset .tabbed-control--next .tabbed-button:after{-webkit-mask-image:var(--md-tabbed-icon--next);mask-image:var(--md-tabbed-icon--next)}@media screen and (max-width:44.984375em){[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels{padding-left:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels{padding-right:.8rem}.md-content__inner>.tabbed-set .tabbed-labels{margin:0 -.8rem;max-width:100vw;scroll-padding-inline-start:.8rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels:after{padding-right:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels:after{padding-left:.8rem}.md-content__inner>.tabbed-set .tabbed-labels:after{content:""}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{padding-left:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{padding-right:.8rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{margin-left:-.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{margin-right:-.8rem}.md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{width:2rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{padding-right:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{padding-left:.8rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{margin-right:-.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{margin-left:-.8rem}.md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{width:2rem}}@media screen{.md-typeset .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9){color:var(--md-default-fg-color);font-weight:500}.md-typeset .no-js .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.md-typeset .no-js .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.md-typeset .no-js .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.md-typeset .no-js .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.md-typeset .no-js .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.md-typeset .no-js .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.md-typeset .no-js .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.md-typeset .no-js .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.md-typeset .no-js .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.md-typeset .no-js .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.md-typeset .no-js .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.md-typeset .no-js .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.md-typeset .no-js .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.md-typeset .no-js .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.md-typeset .no-js .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.md-typeset .no-js .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.md-typeset .no-js .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.md-typeset .no-js .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.md-typeset .no-js .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.md-typeset .no-js .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9),.md-typeset [role=dialog] .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.md-typeset [role=dialog] .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.md-typeset [role=dialog] .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.md-typeset [role=dialog] .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.md-typeset [role=dialog] .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.md-typeset [role=dialog] .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.md-typeset [role=dialog] .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.md-typeset [role=dialog] .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.md-typeset [role=dialog] .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.md-typeset [role=dialog] .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.md-typeset [role=dialog] .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.md-typeset [role=dialog] .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.md-typeset [role=dialog] .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.md-typeset [role=dialog] .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.md-typeset [role=dialog] .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.md-typeset [role=dialog] .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.md-typeset [role=dialog] .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.md-typeset [role=dialog] .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.md-typeset [role=dialog] .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.md-typeset [role=dialog] .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9),.no-js .md-typeset .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.no-js .md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.no-js .md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.no-js .md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.no-js .md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.no-js .md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.no-js .md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.no-js .md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.no-js .md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.no-js .md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.no-js .md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.no-js .md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.no-js .md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.no-js .md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.no-js .md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.no-js .md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.no-js .md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.no-js .md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.no-js .md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.no-js .md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9),[role=dialog] .md-typeset .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,[role=dialog] .md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),[role=dialog] .md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),[role=dialog] .md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),[role=dialog] .md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),[role=dialog] .md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),[role=dialog] .md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),[role=dialog] .md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),[role=dialog] .md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),[role=dialog] .md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),[role=dialog] .md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),[role=dialog] .md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),[role=dialog] .md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),[role=dialog] .md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),[role=dialog] .md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),[role=dialog] .md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),[role=dialog] .md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),[role=dialog] .md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),[role=dialog] .md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),[role=dialog] .md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9){border-color:var(--md-default-fg-color)}}.md-typeset .tabbed-set>input:first-child.focus-visible~.tabbed-labels>:first-child,.md-typeset .tabbed-set>input:nth-child(10).focus-visible~.tabbed-labels>:nth-child(10),.md-typeset .tabbed-set>input:nth-child(11).focus-visible~.tabbed-labels>:nth-child(11),.md-typeset .tabbed-set>input:nth-child(12).focus-visible~.tabbed-labels>:nth-child(12),.md-typeset .tabbed-set>input:nth-child(13).focus-visible~.tabbed-labels>:nth-child(13),.md-typeset .tabbed-set>input:nth-child(14).focus-visible~.tabbed-labels>:nth-child(14),.md-typeset .tabbed-set>input:nth-child(15).focus-visible~.tabbed-labels>:nth-child(15),.md-typeset .tabbed-set>input:nth-child(16).focus-visible~.tabbed-labels>:nth-child(16),.md-typeset .tabbed-set>input:nth-child(17).focus-visible~.tabbed-labels>:nth-child(17),.md-typeset .tabbed-set>input:nth-child(18).focus-visible~.tabbed-labels>:nth-child(18),.md-typeset .tabbed-set>input:nth-child(19).focus-visible~.tabbed-labels>:nth-child(19),.md-typeset .tabbed-set>input:nth-child(2).focus-visible~.tabbed-labels>:nth-child(2),.md-typeset .tabbed-set>input:nth-child(20).focus-visible~.tabbed-labels>:nth-child(20),.md-typeset .tabbed-set>input:nth-child(3).focus-visible~.tabbed-labels>:nth-child(3),.md-typeset .tabbed-set>input:nth-child(4).focus-visible~.tabbed-labels>:nth-child(4),.md-typeset .tabbed-set>input:nth-child(5).focus-visible~.tabbed-labels>:nth-child(5),.md-typeset .tabbed-set>input:nth-child(6).focus-visible~.tabbed-labels>:nth-child(6),.md-typeset .tabbed-set>input:nth-child(7).focus-visible~.tabbed-labels>:nth-child(7),.md-typeset .tabbed-set>input:nth-child(8).focus-visible~.tabbed-labels>:nth-child(8),.md-typeset .tabbed-set>input:nth-child(9).focus-visible~.tabbed-labels>:nth-child(9){color:var(--md-accent-fg-color)}.md-typeset .tabbed-set>input:first-child:checked~.tabbed-content>:first-child,.md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-content>:nth-child(10),.md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-content>:nth-child(11),.md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-content>:nth-child(12),.md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-content>:nth-child(13),.md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-content>:nth-child(14),.md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-content>:nth-child(15),.md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-content>:nth-child(16),.md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-content>:nth-child(17),.md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-content>:nth-child(18),.md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-content>:nth-child(19),.md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-content>:nth-child(2),.md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-content>:nth-child(20),.md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-content>:nth-child(3),.md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-content>:nth-child(4),.md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-content>:nth-child(5),.md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-content>:nth-child(6),.md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-content>:nth-child(7),.md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-content>:nth-child(8),.md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-content>:nth-child(9){display:block}:root{--md-tasklist-icon:url('data:image/svg+xml;charset=utf-8, ');--md-tasklist-icon--checked:url('data:image/svg+xml;charset=utf-8, ')}.md-typeset .task-list-item{list-style-type:none;position:relative}[dir=ltr] .md-typeset .task-list-item [type=checkbox]{left:-2em}[dir=rtl] .md-typeset .task-list-item [type=checkbox]{right:-2em}.md-typeset .task-list-item [type=checkbox]{position:absolute;top:.45em}.md-typeset .task-list-control [type=checkbox]{opacity:0;z-index:-1}[dir=ltr] .md-typeset .task-list-indicator:before{left:-1.5em}[dir=rtl] .md-typeset .task-list-indicator:before{right:-1.5em}.md-typeset .task-list-indicator:before{background-color:var(--md-default-fg-color--lighter);content:"";height:1.25em;-webkit-mask-image:var(--md-tasklist-icon);mask-image:var(--md-tasklist-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.25em;width:1.25em}.md-typeset [type=checkbox]:checked+.task-list-indicator:before{background-color:#00e676;-webkit-mask-image:var(--md-tasklist-icon--checked);mask-image:var(--md-tasklist-icon--checked)}@media print{.giscus,[id=__comments]{display:none}}:root>*{--md-mermaid-font-family:var(--md-text-font-family),sans-serif;--md-mermaid-edge-color:var(--md-code-fg-color);--md-mermaid-node-bg-color:var(--md-accent-fg-color--transparent);--md-mermaid-node-fg-color:var(--md-accent-fg-color);--md-mermaid-label-bg-color:var(--md-default-bg-color);--md-mermaid-label-fg-color:var(--md-code-fg-color);--md-mermaid-sequence-actor-bg-color:var(--md-mermaid-label-bg-color);--md-mermaid-sequence-actor-fg-color:var(--md-mermaid-label-fg-color);--md-mermaid-sequence-actor-border-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-actor-line-color:var(--md-default-fg-color--lighter);--md-mermaid-sequence-actorman-bg-color:var(--md-mermaid-label-bg-color);--md-mermaid-sequence-actorman-line-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-box-bg-color:var(--md-mermaid-node-bg-color);--md-mermaid-sequence-box-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-label-bg-color:var(--md-mermaid-node-bg-color);--md-mermaid-sequence-label-fg-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-loop-bg-color:var(--md-mermaid-node-bg-color);--md-mermaid-sequence-loop-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-loop-border-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-message-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-message-line-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-note-bg-color:var(--md-mermaid-label-bg-color);--md-mermaid-sequence-note-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-note-border-color:var(--md-mermaid-label-fg-color);--md-mermaid-sequence-number-bg-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-number-fg-color:var(--md-accent-bg-color)}.mermaid{line-height:normal;margin:1em 0}.md-typeset .grid{grid-gap:.4rem;display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,16rem),1fr));margin:1em 0}.md-typeset .grid.cards>ol,.md-typeset .grid.cards>ul{display:contents}.md-typeset .grid.cards>ol>li,.md-typeset .grid.cards>ul>li,.md-typeset .grid>.card{border:.05rem solid var(--md-default-fg-color--lightest);border-radius:.4rem;display:block;margin:0;padding:.8rem;transition:background-color .25s,border .25s,box-shadow .25s}.md-typeset .grid.cards>ol>li:focus-within,.md-typeset .grid.cards>ol>li:hover,.md-typeset .grid.cards>ul>li:focus-within,.md-typeset .grid.cards>ul>li:hover,.md-typeset .grid>.card:focus-within,.md-typeset .grid>.card:hover{border-color:#0000;box-shadow:var(--md-shadow-z2)}.md-typeset .grid.cards>ol>li>hr,.md-typeset .grid.cards>ul>li>hr,.md-typeset .grid>.card>hr{margin-bottom:1em;margin-top:1em}.md-typeset .grid.cards>ol>li>:first-child,.md-typeset .grid.cards>ul>li>:first-child,.md-typeset .grid>.card>:first-child{margin-top:0}.md-typeset .grid.cards>ol>li>:last-child,.md-typeset .grid.cards>ul>li>:last-child,.md-typeset .grid>.card>:last-child{margin-bottom:0}.md-typeset .grid>*,.md-typeset .grid>.admonition,.md-typeset .grid>.highlight>*,.md-typeset .grid>.highlighttable,.md-typeset .grid>.md-typeset details,.md-typeset .grid>details,.md-typeset .grid>pre{margin-bottom:0;margin-top:0}.md-typeset .grid>.highlight>pre:only-child,.md-typeset .grid>.highlight>pre>code,.md-typeset .grid>.highlighttable,.md-typeset .grid>.highlighttable>tbody,.md-typeset .grid>.highlighttable>tbody>tr,.md-typeset .grid>.highlighttable>tbody>tr>.code,.md-typeset .grid>.highlighttable>tbody>tr>.code>.highlight,.md-typeset .grid>.highlighttable>tbody>tr>.code>.highlight>pre,.md-typeset .grid>.highlighttable>tbody>tr>.code>.highlight>pre>code{height:100%}.md-typeset .grid>.tabbed-set{margin-bottom:0;margin-top:0}@media screen and (min-width:45em){[dir=ltr] .md-typeset .inline{float:left}[dir=rtl] .md-typeset .inline{float:right}[dir=ltr] .md-typeset .inline{margin-right:.8rem}[dir=rtl] .md-typeset .inline{margin-left:.8rem}.md-typeset .inline{margin-bottom:.8rem;margin-top:0;width:11.7rem}[dir=ltr] .md-typeset .inline.end{float:right}[dir=rtl] .md-typeset .inline.end{float:left}[dir=ltr] .md-typeset .inline.end{margin-left:.8rem;margin-right:0}[dir=rtl] .md-typeset .inline.end{margin-left:0;margin-right:.8rem}}
\ No newline at end of file
diff --git a/v2.2/assets/stylesheets/modern/palette.dfe2e883.min.css b/v2.2/assets/stylesheets/modern/palette.dfe2e883.min.css
new file mode 100644
index 0000000..d58a561
--- /dev/null
+++ b/v2.2/assets/stylesheets/modern/palette.dfe2e883.min.css
@@ -0,0 +1 @@
+@media screen{[data-md-color-scheme=slate]{--md-default-fg-color:hsla(var(--md-hue),15%,90%,0.82);--md-default-fg-color--light:hsla(var(--md-hue),15%,90%,0.56);--md-default-fg-color--lighter:hsla(var(--md-hue),15%,90%,0.32);--md-default-fg-color--lightest:hsla(var(--md-hue),15%,90%,0.12);--md-default-bg-color:hsla(var(--md-hue),15%,5%,1);--md-default-bg-color--light:hsla(var(--md-hue),15%,5%,0.54);--md-default-bg-color--lighter:hsla(var(--md-hue),15%,5%,0.26);--md-default-bg-color--lightest:hsla(var(--md-hue),15%,5%,0.07);--md-code-fg-color:hsla(var(--md-hue),20%,80%,1);--md-code-bg-color:hsla(var(--md-hue),20%,10%,1);--md-code-bg-color--light:hsla(var(--md-hue),20%,10%,0.9);--md-code-bg-color--lighter:hsla(var(--md-hue),20%,10%,0.54);--md-code-hl-color:#2977ff;--md-code-hl-color--light:#2977ff1a;--md-code-hl-number-color:#e6695b;--md-code-hl-special-color:#f06090;--md-code-hl-function-color:#c973d9;--md-code-hl-constant-color:#9383e2;--md-code-hl-keyword-color:#6791e0;--md-code-hl-string-color:#2fb170;--md-code-hl-name-color:var(--md-code-fg-color);--md-code-hl-operator-color:var(--md-default-fg-color--light);--md-code-hl-punctuation-color:var(--md-default-fg-color--light);--md-code-hl-comment-color:var(--md-default-fg-color--light);--md-code-hl-generic-color:var(--md-default-fg-color--light);--md-code-hl-variable-color:var(--md-default-fg-color--light);--md-typeset-color:var(--md-default-fg-color);--md-typeset-a-color:var(--md-primary-fg-color);--md-typeset-kbd-color:hsla(var(--md-hue),15%,90%,0.12);--md-typeset-kbd-accent-color:hsla(var(--md-hue),15%,90%,0.2);--md-typeset-kbd-border-color:hsla(var(--md-hue),15%,14%,1);--md-typeset-mark-color:#4287ff4d;--md-typeset-table-color:hsla(var(--md-hue),15%,95%,0.12);--md-typeset-table-color--light:hsla(var(--md-hue),15%,95%,0.035);--md-admonition-fg-color:var(--md-default-fg-color);--md-admonition-bg-color:var(--md-default-bg-color);--md-footer-bg-color:hsla(var(--md-hue),15%,10%,0.87);--md-footer-bg-color--dark:hsla(var(--md-hue),15%,8%,1);--md-shadow-z1:0 0.2rem 0.5rem #0000000d,0 0 0.05rem #ffffff1a;--md-shadow-z2:0 0.2rem 0.5rem #00000040,0 0 0.05rem #ffffff59;--md-shadow-z3:0 0.5rem 2rem #0006,0 0 0.05rem #00000059;color-scheme:dark}[data-md-color-scheme=slate] .md-header__title,[data-md-color-scheme=slate] h1,[data-md-color-scheme=slate] h2,[data-md-color-scheme=slate] h3,[data-md-color-scheme=slate] h4,[data-md-color-scheme=slate] h5,[data-md-color-scheme=slate] h6{color:hsla(var(--md-hue),0%,100%,1)}[data-md-color-scheme=slate] img[src$="#gh-light-mode-only"],[data-md-color-scheme=slate] img[src$="#only-light"]{display:none}[data-md-color-scheme=slate]{--color-foreground:255 255 255;--color-background:22 23 26;--color-background-subtle:33 34 38;--color-backdrop:11 12 15}[data-md-color-scheme=slate][data-md-color-primary=pink]{--md-typeset-a-color:#ed5487}[data-md-color-scheme=slate][data-md-color-primary=purple]{--md-typeset-a-color:#c46fd3}[data-md-color-scheme=slate][data-md-color-primary=deep-purple]{--md-typeset-a-color:#a47bea}[data-md-color-scheme=slate][data-md-color-primary=indigo]{--md-typeset-a-color:#5488e8}[data-md-color-scheme=slate][data-md-color-primary=teal]{--md-typeset-a-color:#00ccb8}[data-md-color-scheme=slate][data-md-color-primary=green]{--md-typeset-a-color:#71c174}[data-md-color-scheme=slate][data-md-color-primary=deep-orange]{--md-typeset-a-color:#ff764d}[data-md-color-scheme=slate][data-md-color-primary=brown]{--md-typeset-a-color:#c1775c}[data-md-color-scheme=slate][data-md-color-primary=black],[data-md-color-scheme=slate][data-md-color-primary=blue-grey],[data-md-color-scheme=slate][data-md-color-primary=grey],[data-md-color-scheme=slate][data-md-color-primary=white]{--md-typeset-a-color:#5e8bde}[data-md-color-switching] *,[data-md-color-switching] :after,[data-md-color-switching] :before{transition-duration:0ms!important}}[data-md-color-accent=red]{--md-accent-fg-color:#ff1947;--md-accent-fg-color--transparent:#ff19471a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=pink]{--md-accent-fg-color:#f50056;--md-accent-fg-color--transparent:#f500561a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=purple]{--md-accent-fg-color:#df41fb;--md-accent-fg-color--transparent:#df41fb1a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=deep-purple]{--md-accent-fg-color:#7c4dff;--md-accent-fg-color--transparent:#7c4dff1a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=indigo]{--md-accent-fg-color:#526cfe;--md-accent-fg-color--transparent:#526cfe1a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=blue]{--md-accent-fg-color:#4287ff;--md-accent-fg-color--transparent:#4287ff1a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=light-blue]{--md-accent-fg-color:#0091eb;--md-accent-fg-color--transparent:#0091eb1a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=cyan]{--md-accent-fg-color:#00bad6;--md-accent-fg-color--transparent:#00bad61a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=teal]{--md-accent-fg-color:#00bda4;--md-accent-fg-color--transparent:#00bda41a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=green]{--md-accent-fg-color:#00c753;--md-accent-fg-color--transparent:#00c7531a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=light-green]{--md-accent-fg-color:#63de17;--md-accent-fg-color--transparent:#63de171a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=lime]{--md-accent-fg-color:#b0eb00;--md-accent-fg-color--transparent:#b0eb001a;--md-accent-bg-color:#000000de;--md-accent-bg-color--light:#0000008a}[data-md-color-accent=yellow]{--md-accent-fg-color:#ffd500;--md-accent-fg-color--transparent:#ffd5001a;--md-accent-bg-color:#000000de;--md-accent-bg-color--light:#0000008a}[data-md-color-accent=amber]{--md-accent-fg-color:#fa0;--md-accent-fg-color--transparent:#ffaa001a;--md-accent-bg-color:#000000de;--md-accent-bg-color--light:#0000008a}[data-md-color-accent=orange]{--md-accent-fg-color:#ff9100;--md-accent-fg-color--transparent:#ff91001a;--md-accent-bg-color:#000000de;--md-accent-bg-color--light:#0000008a}[data-md-color-accent=deep-orange]{--md-accent-fg-color:#ff6e42;--md-accent-fg-color--transparent:#ff6e421a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-primary=red]{--md-primary-fg-color:#ef5552;--md-primary-fg-color--light:#e57171;--md-primary-fg-color--dark:#e53734;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=pink]{--md-primary-fg-color:#e92063;--md-primary-fg-color--light:#ec417a;--md-primary-fg-color--dark:#c3185d;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=purple]{--md-primary-fg-color:#ab47bd;--md-primary-fg-color--light:#bb69c9;--md-primary-fg-color--dark:#8c24a8;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=deep-purple]{--md-primary-fg-color:#7e56c2;--md-primary-fg-color--light:#9574cd;--md-primary-fg-color--dark:#673ab6;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=indigo]{--md-primary-fg-color:#4051b5;--md-primary-fg-color--light:#5d6cc0;--md-primary-fg-color--dark:#303fa1;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=blue]{--md-primary-fg-color:#2094f3;--md-primary-fg-color--light:#42a5f5;--md-primary-fg-color--dark:#1975d2;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=light-blue]{--md-primary-fg-color:#02a6f2;--md-primary-fg-color--light:#28b5f6;--md-primary-fg-color--dark:#0287cf;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=cyan]{--md-primary-fg-color:#00bdd6;--md-primary-fg-color--light:#25c5da;--md-primary-fg-color--dark:#0097a8;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=teal]{--md-primary-fg-color:#009485;--md-primary-fg-color--light:#26a699;--md-primary-fg-color--dark:#007a6c;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=green]{--md-primary-fg-color:#4cae4f;--md-primary-fg-color--light:#68bb6c;--md-primary-fg-color--dark:#398e3d;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=light-green]{--md-primary-fg-color:#8bc34b;--md-primary-fg-color--light:#9ccc66;--md-primary-fg-color--dark:#689f38;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=lime]{--md-primary-fg-color:#cbdc38;--md-primary-fg-color--light:#d3e156;--md-primary-fg-color--dark:#b0b52c;--md-primary-bg-color:#000000de;--md-primary-bg-color--light:#0000008a}[data-md-color-primary=yellow]{--md-primary-fg-color:#ffec3d;--md-primary-fg-color--light:#ffee57;--md-primary-fg-color--dark:#fbc02d;--md-primary-bg-color:#000000de;--md-primary-bg-color--light:#0000008a}[data-md-color-primary=amber]{--md-primary-fg-color:#ffc105;--md-primary-fg-color--light:#ffc929;--md-primary-fg-color--dark:#ffa200;--md-primary-bg-color:#000000de;--md-primary-bg-color--light:#0000008a}[data-md-color-primary=orange]{--md-primary-fg-color:#ffa724;--md-primary-fg-color--light:#ffa724;--md-primary-fg-color--dark:#fa8900;--md-primary-bg-color:#000000de;--md-primary-bg-color--light:#0000008a}[data-md-color-primary=deep-orange]{--md-primary-fg-color:#ff6e42;--md-primary-fg-color--light:#ff8a66;--md-primary-fg-color--dark:#f4511f;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=brown]{--md-primary-fg-color:#795649;--md-primary-fg-color--light:#8d6e62;--md-primary-fg-color--dark:#5d4037;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=grey]{--md-primary-fg-color:#757575;--md-primary-fg-color--light:#9e9e9e;--md-primary-fg-color--dark:#616161;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3;--md-typeset-a-color:#4051b5}[data-md-color-primary=blue-grey]{--md-primary-fg-color:#546d78;--md-primary-fg-color--light:#607c8a;--md-primary-fg-color--dark:#455a63;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3;--md-typeset-a-color:#4051b5}[data-md-color-primary=light-green]:not([data-md-color-scheme=slate]){--md-typeset-a-color:#72ad2e}[data-md-color-primary=lime]:not([data-md-color-scheme=slate]){--md-typeset-a-color:#8b990a}[data-md-color-primary=yellow]:not([data-md-color-scheme=slate]){--md-typeset-a-color:#b8a500}[data-md-color-primary=amber]:not([data-md-color-scheme=slate]){--md-typeset-a-color:#d19d00}[data-md-color-primary=orange]:not([data-md-color-scheme=slate]){--md-typeset-a-color:#e68a00}
\ No newline at end of file
diff --git a/v2.2/examples/pagination-search/index.html b/v2.2/examples/pagination-search/index.html
new file mode 100644
index 0000000..72fc97e
--- /dev/null
+++ b/v2.2/examples/pagination-search/index.html
@@ -0,0 +1,1958 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Pagination & search - FastAPI Toolsets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Home
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Examples
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+This example builds an articles listing endpoint that supports offset pagination , cursor pagination , full-text search , faceted filtering , and sorting — all from a single CrudFactory definition.
+Models
+models.py import uuid
+
+from sqlalchemy import Boolean , ForeignKey , String , Text
+from sqlalchemy.orm import DeclarativeBase , Mapped , mapped_column , relationship
+
+from fastapi_toolsets.models import CreatedAtMixin
+
+
+class Base ( DeclarativeBase ):
+ pass
+
+
+class Category ( Base ):
+ __tablename__ = "categories"
+
+ id : Mapped [ uuid . UUID ] = mapped_column ( primary_key = True , default = uuid . uuid4 )
+ name : Mapped [ str ] = mapped_column ( String ( 64 ), unique = True )
+
+ articles : Mapped [ list [ "Article" ]] = relationship ( back_populates = "category" )
+
+
+class Article ( Base , CreatedAtMixin ):
+ __tablename__ = "articles"
+
+ id : Mapped [ uuid . UUID ] = mapped_column ( primary_key = True , default = uuid . uuid4 )
+ title : Mapped [ str ] = mapped_column ( String ( 256 ))
+ body : Mapped [ str ] = mapped_column ( Text )
+ status : Mapped [ str ] = mapped_column ( String ( 32 ))
+ published : Mapped [ bool ] = mapped_column ( Boolean , default = False )
+ category_id : Mapped [ uuid . UUID | None ] = mapped_column (
+ ForeignKey ( "categories.id" ), nullable = True
+ )
+
+ category : Mapped [ "Category | None" ] = relationship ( back_populates = "articles" )
+
+Schemas
+schemas.py import datetime
+import uuid
+
+from fastapi_toolsets.schemas import PydanticBase
+
+
+class ArticleRead ( PydanticBase ):
+ id : uuid . UUID
+ created_at : datetime . datetime
+ title : str
+ status : str
+ published : bool
+ category_id : uuid . UUID | None
+
+Crud
+Declare searchable_fields, facet_fields, and order_fields once on CrudFactory . All endpoints built from this class share the same defaults and can override them per call.
+crud.py from fastapi_toolsets.crud import CrudFactory
+
+from .models import Article , Category
+
+ArticleCrud = CrudFactory (
+ model = Article ,
+ cursor_column = Article . created_at ,
+ searchable_fields = [ # default fields for full-text search
+ Article . title ,
+ Article . body ,
+ ( Article . category , Category . name ),
+ ],
+ facet_fields = [ # fields exposed as filter dropdowns
+ Article . status ,
+ ( Article . category , Category . name ),
+ ],
+ order_fields = [ # fields exposed for client-driven ordering
+ Article . title ,
+ Article . created_at ,
+ ],
+)
+
+Session dependency
+db.py from typing import Annotated
+
+from fastapi import Depends
+from sqlalchemy.ext.asyncio import AsyncSession , async_sessionmaker , create_async_engine
+
+from fastapi_toolsets.db import create_db_context , create_db_dependency
+
+DATABASE_URL = "postgresql+asyncpg://postgres:postgres@localhost:5432/postgres"
+
+engine = create_async_engine ( url = DATABASE_URL , future = True )
+async_session_maker = async_sessionmaker ( bind = engine , expire_on_commit = False )
+
+get_db = create_db_dependency ( session_maker = async_session_maker )
+get_db_context = create_db_context ( session_maker = async_session_maker )
+
+
+SessionDep = Annotated [ AsyncSession , Depends ( get_db )]
+
+
+
Deploy a Postgres DB with docker
+
docker run -d --name postgres -e POSTGRES_USER = postgres -e POSTGRES_PASSWORD = postgres -e POSTGRES_DB = postgres -p 5432 :5432 postgres:18-alpine
+
+
+App
+app.py from fastapi import FastAPI
+
+from fastapi_toolsets.exceptions import init_exceptions_handlers
+
+from .routes import router
+
+app = FastAPI ()
+init_exceptions_handlers ( app = app )
+app . include_router ( router = router )
+
+Routes
+
+Best for admin panels or any UI that needs a total item count and numbered pages.
+routes.py:1:36 from typing import Annotated
+
+from fastapi import APIRouter , Depends
+
+from fastapi_toolsets.crud import OrderByClause
+from fastapi_toolsets.schemas import (
+ CursorPaginatedResponse ,
+ OffsetPaginatedResponse ,
+ PaginatedResponse ,
+)
+
+from .crud import ArticleCrud
+from .db import SessionDep
+from .models import Article
+from .schemas import ArticleRead
+
+router = APIRouter ( prefix = "/articles" )
+
+
+@router . get ( "/offset" )
+async def list_articles_offset (
+ session : SessionDep ,
+ params : Annotated [
+ dict ,
+ Depends ( ArticleCrud . offset_params ( default_page_size = 20 , max_page_size = 100 )),
+ ],
+ filter_by : Annotated [ dict [ str , list [ str ]], Depends ( ArticleCrud . filter_params ())],
+ order_by : Annotated [
+ OrderByClause | None ,
+ Depends ( ArticleCrud . order_params ( default_field = Article . created_at )),
+ ],
+ search : str | None = None ,
+) -> OffsetPaginatedResponse [ ArticleRead ]:
+ return await ArticleCrud . offset_paginate (
+ session = session ,
+ ** params ,
+
+Example request
+ GET /articles/offset?page=2&items_per_page=10&search=fastapi&status=published&order_by=title&order=asc
+
+Example response
+{
+ "status" : "SUCCESS" ,
+ "data" : [
+ { "id" : "3f47ac69-..." , "title" : "FastAPI tips" , "status" : "published" , ... }
+ ],
+ "pagination" : {
+ "total_count" : 42 ,
+ "page" : 2 ,
+ "items_per_page" : 10 ,
+ "has_more" : true
+ },
+ "filter_attributes" : {
+ "status" : [ "archived" , "draft" , "published" ],
+ "name" : [ "backend" , "frontend" , "python" ]
+ }
+}
+
+filter_attributes always reflects the values visible after applying the active filters. Use it to populate filter dropdowns on the client.
+
+Best for feeds, infinite scroll, or any high-throughput API where offset performance degrades.
+routes.py:39:59 order_by = order_by ,
+ schema = ArticleRead ,
+ )
+
+
+@router . get ( "/cursor" )
+async def list_articles_cursor (
+ session : SessionDep ,
+ params : Annotated [
+ dict ,
+ Depends ( ArticleCrud . cursor_params ( default_page_size = 20 , max_page_size = 100 )),
+ ],
+ filter_by : Annotated [ dict [ str , list [ str ]], Depends ( ArticleCrud . filter_params ())],
+ order_by : Annotated [
+ OrderByClause | None ,
+ Depends ( ArticleCrud . order_params ( default_field = Article . created_at )),
+ ],
+ search : str | None = None ,
+) -> CursorPaginatedResponse [ ArticleRead ]:
+ return await ArticleCrud . cursor_paginate (
+ session = session ,
+
+Example request
+ GET /articles/cursor?items_per_page=10&status=published&order_by=created_at&order=desc
+
+Example response
+{
+ "status" : "SUCCESS" ,
+ "data" : [
+ { "id" : "3f47ac69-..." , "title" : "FastAPI tips" , "status" : "published" , ... }
+ ],
+ "pagination" : {
+ "next_cursor" : "eyJ2YWx1ZSI6ICIzZjQ3YWM2OS0uLi4ifQ==" ,
+ "prev_cursor" : null ,
+ "items_per_page" : 10 ,
+ "has_more" : true
+ },
+ "filter_attributes" : {
+ "status" : [ "published" ],
+ "name" : [ "backend" , "python" ]
+ }
+}
+
+Pass next_cursor as the cursor query parameter on the next request to advance to the next page.
+Search behaviour
+Both endpoints inherit the same searchable_fields declared on ArticleCrud:
+Search is case-insensitive and uses a LIKE %query% pattern. Pass a SearchConfig instead of a plain string to control case sensitivity or switch to match_mode="all" (AND across all fields instead of OR).
+from fastapi_toolsets.crud import SearchConfig
+
+# Both title AND body must contain "fastapi"
+result = await ArticleCrud . offset_paginate (
+ session ,
+ search = SearchConfig ( query = "fastapi" , case_sensitive = True , match_mode = "all" ),
+ search_fields = [ Article . title , Article . body ],
+)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back to top
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v2.2/index.html b/v2.2/index.html
new file mode 100644
index 0000000..e4d66b3
--- /dev/null
+++ b/v2.2/index.html
@@ -0,0 +1,1677 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ FastAPI Toolsets - FastAPI Toolsets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+A modular collection of production-ready utilities for FastAPI. Install only what you need — from async CRUD and database helpers to CLI tooling, Prometheus metrics, and pytest fixtures. Each module is independently installable via optional extras, keeping your dependency footprint minimal.
+
+
+
+
+
+
+
+
+Documentation : https://fastapi-toolsets.d3vyce.fr
+Source Code : https://github.com/d3vyce/fastapi-toolsets
+
+Installation
+The base package includes the core modules (CRUD, database, schemas, exceptions, fixtures, dependencies, model mixins, logging):
+ uv add fastapi-toolsets
+
+Install only the extras you need:
+ uv add "fastapi-toolsets[cli]"
+ uv add "fastapi-toolsets[metrics]"
+ uv add "fastapi-toolsets[pytest]"
+
+Or install everything:
+ uv add "fastapi-toolsets[all]"
+
+Features
+Core
+
+CRUD : Generic async CRUD operations with CrudFactory, built-in full-text/faceted search and Offset/Cursor pagination.
+Database : Session management, transaction helpers, table locking, and polling-based row change detection
+Dependencies : FastAPI dependency factories (PathDependency, BodyDependency) for automatic DB lookups from path or body parameters
+Fixtures : Fixture system with dependency management, context support, and pytest integration
+Model Mixins : SQLAlchemy mixins for common column patterns (UUIDMixin, CreatedAtMixin, UpdatedAtMixin, TimestampMixin)
+Standardized API Responses : Consistent response format with Response, PaginatedResponse, and PydanticBase
+Exception Handling : Structured error responses with automatic OpenAPI documentation
+Logging : Logging configuration with uvicorn integration via configure_logging and get_logger
+
+Optional
+
+CLI : Django-like command-line interface with fixture management and custom commands support
+Metrics : Prometheus metrics endpoint with provider/collector registry
+Pytest Helpers : Async test client, database session management, pytest-xdist support, and table cleanup utilities
+
+License
+MIT License - see LICENSE for details.
+Contributing
+Contributions are welcome! Please feel free to submit issues and pull requests.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back to top
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v2.2/migration/v2/index.html b/v2.2/migration/v2/index.html
new file mode 100644
index 0000000..336eee5
--- /dev/null
+++ b/v2.2/migration/v2/index.html
@@ -0,0 +1,1849 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Migrating to v2.0 - FastAPI Toolsets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Home
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Migration
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Migrating to v2.0
+This page covers every breaking change introduced in v2.0 and the steps required to update your code.
+
+CRUD
+schema is now required in offset_paginate() and cursor_paginate()
+Calls that omit schema will now raise a TypeError at runtime.
+Previously schema was optional; omitting it returned raw SQLAlchemy model instances inside the response. It is now a required keyword argument and the response always contains serialized schema instances.
+Before (v1) Now (v2)
+
+
+
# schema omitted — returned raw model instances
+result = await UserCrud . offset_paginate ( session = session , page = 1 )
+result = await UserCrud . cursor_paginate ( session = session , cursor = token )
+
+
+
+
result = await UserCrud . offset_paginate ( session = session , page = 1 , schema = UserRead )
+result = await UserCrud . cursor_paginate ( session = session , cursor = token , schema = UserRead )
+
+
+
+
+as_response removed from create(), get(), and update()
+Passing as_response to these methods will raise a TypeError at runtime.
+The as_response=True shorthand is replaced by passing a schema directly. The return value is a Response[schema] when schema is provided, or the raw model instance when it is not.
+Before (v1) Now (v2)
+
+
+
user = await UserCrud . create ( session = session , obj = data , as_response = True )
+user = await UserCrud . get ( session = session , filters = filters , as_response = True )
+user = await UserCrud . update ( session = session , obj = data , filters , as_response = True )
+
+
+
+
user = await UserCrud . create ( session = session , obj = data , schema = UserRead )
+user = await UserCrud . get ( session = session , filters = filters , schema = UserRead )
+user = await UserCrud . update ( session = session , obj = data , filters , schema = UserRead )
+
+
+
+
+delete(): as_response renamed and return type changed
+as_response is gone, and the plain (non-response) call no longer returns True.
+Two changes were made to delete():
+
+The as_response parameter is renamed to return_response.
+When called without return_response=True, the method now returns None on success instead of True.
+
+Before (v1) Now (v2)
+
+
+
ok = await UserCrud . delete ( session = session , filters = filters )
+if ok : # True on success
+ ...
+
+response = await UserCrud . delete ( session = session , filters = filters , as_response = True )
+
+
+
+
await UserCrud . delete ( session = session , filters = filters ) # returns None
+
+response = await UserCrud . delete ( session = session , filters = filters , return_response = True )
+
+
+
+
+paginate() alias removed
+Any call to crud.paginate(...) will raise AttributeError at runtime.
+The paginate shorthand was an alias for offset_paginate. It has been removed; call offset_paginate directly.
+Before (v1) Now (v2)
+
+
+
result = await UserCrud . paginate ( session = session , page = 2 , items_per_page = 20 , schema = UserRead )
+
+
+
+
result = await UserCrud . offset_paginate ( session = session , page = 2 , items_per_page = 20 , schema = UserRead )
+
+
+
+
+
+Exceptions
+Missing api_error raises TypeError at class definition time
+Unfinished or stub exception subclasses that previously compiled fine will now fail on import.
+In v1, a subclass without api_error would only fail when the exception was raised. In v2, __init_subclass__ validates this at class definition time.
+Before (v1) Now (v2)
+
+
+
class MyError ( ApiException ):
+ pass # fine until raised
+
+
+
+
class MyError ( ApiException ):
+ pass # TypeError: MyError must define an 'api_error' class attribute.
+
+
+
+
+For shared base classes that are not meant to be raised directly, use abstract=True:
+class BillingError ( ApiException , abstract = True ):
+ """Base for all billing-related errors — not raised directly."""
+
+class PaymentRequiredError ( BillingError ):
+ api_error = ApiError ( code = 402 , msg = "Payment Required" , desc = "..." , err_code = "BILLING-402" )
+
+
+Schemas
+
+Pagination was already deprecated in v1 and is fully removed in v2, you now need to use OffsetPagination or CursorPagination .
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back to top
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v2.2/module/cli/index.html b/v2.2/module/cli/index.html
new file mode 100644
index 0000000..c870f26
--- /dev/null
+++ b/v2.2/module/cli/index.html
@@ -0,0 +1,1751 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ CLI - FastAPI Toolsets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Home
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+CLI
+Typer-based command-line interface for managing your FastAPI application, with built-in fixture commands integration.
+Installation
+uv pip
+
+
+
uv add "fastapi-toolsets[cli]"
+
+
+
+
pip install "fastapi-toolsets[cli]"
+
+
+
+
+Overview
+The cli module provides a manager entry point built with Typer . It allow custom commands to be added in addition of the fixture commands when a FixtureRegistry and a database context are configured.
+Configuration
+Configure the CLI in your pyproject.toml:
+[tool.fastapi-toolsets]
+cli = "myapp.cli:cli" # Custom Typer app
+fixtures = "myapp.fixtures:registry" # FixtureRegistry instance
+db_context = "myapp.db:db_context" # Async context manager for sessions
+
+All fields are optional. Without configuration, the manager command still works but no command are available.
+Usage
+# Manager commands
+ manager --help
+
+ Usage: manager [ OPTIONS] COMMAND [ ARGS] ...
+
+ FastAPI utilities CLI.
+
+ ╭─ Options ────────────────────────────────────────────────────────────────────────╮
+ │ --install-completion Install completion for the current shell. │
+ │ --show-completion Show completion for the current shell, to copy it │
+ │ or customize the installation. │
+ │ --help Show this message and exit. │
+ ╰──────────────────────────────────────────────────────────────────────────────────╯
+ ╭─ Commands ───────────────────────────────────────────────────────────────────────╮
+ │ check-db │
+ │ fixtures Manage database fixtures. │
+ ╰──────────────────────────────────────────────────────────────────────────────────╯
+
+# Fixtures commands
+ manager fixtures --help
+
+ Usage: manager fixtures [ OPTIONS] COMMAND [ ARGS] ...
+
+ Manage database fixtures.
+
+ ╭─ Options ────────────────────────────────────────────────────────────────────────╮
+ │ --help Show this message and exit. │
+ ╰──────────────────────────────────────────────────────────────────────────────────╯
+ ╭─ Commands ───────────────────────────────────────────────────────────────────────╮
+ │ list List all registered fixtures. │
+ │ load Load fixtures into the database. │
+ ╰──────────────────────────────────────────────────────────────────────────────────╯
+
+Custom CLI
+You can extend the CLI by providing your own Typer app. The manager entry point will merge your app's commands with the built-in ones:
+# myapp/cli.py
+import typer
+
+cli = typer . Typer ()
+
+@cli . command ()
+def hello ():
+ print ( "Hello from my app!" )
+
+[tool.fastapi-toolsets]
+cli = "myapp.cli:cli"
+
+
+ API Reference
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back to top
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v2.2/module/crud/index.html b/v2.2/module/crud/index.html
new file mode 100644
index 0000000..a88b3f9
--- /dev/null
+++ b/v2.2/module/crud/index.html
@@ -0,0 +1,2337 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ CRUD - FastAPI Toolsets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Home
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+CRUD
+Generic async CRUD operations for SQLAlchemy models with search, pagination, and many-to-many support.
+
+
Info
+
This module has been coded and tested to be compatible with PostgreSQL only.
+
+Overview
+The crud module provides AsyncCrud , an abstract base class with a full suite of async database operations, and CrudFactory , a convenience function to instantiate it for a given model.
+Creating a CRUD class
+from fastapi_toolsets.crud import CrudFactory
+from myapp.models import User
+
+UserCrud = CrudFactory ( model = User )
+
+CrudFactory dynamically creates a class named AsyncUserCrud with User as its model.
+Basic operations
+
+
get_or_none added in v2.2
+
+# Create
+user = await UserCrud . create ( session = session , obj = UserCreateSchema ( username = "alice" ))
+
+# Get one (raises NotFoundError if not found)
+user = await UserCrud . get ( session = session , filters = [ User . id == user_id ])
+
+# Get one or None (never raises)
+user = await UserCrud . get_or_none ( session = session , filters = [ User . id == user_id ])
+
+# Get first or None
+user = await UserCrud . first ( session = session , filters = [ User . email == email ])
+
+# Get multiple
+users = await UserCrud . get_multi ( session = session , filters = [ User . is_active == True ])
+
+# Update
+user = await UserCrud . update ( session = session , obj = UserUpdateSchema ( username = "bob" ), filters = [ User . id == user_id ])
+
+# Delete
+await UserCrud . delete ( session = session , filters = [ User . id == user_id ])
+
+# Count / exists
+count = await UserCrud . count ( session = session , filters = [ User . is_active == True ])
+exists = await UserCrud . exists ( session = session , filters = [ User . email == email ])
+
+Fetching a single record
+Three methods fetch a single record — choose based on how you want to handle the "not found" case and whether you need strict uniqueness:
+
+
+
+Method
+Not found
+Multiple results
+
+
+
+
+get
+raises NotFoundError
+raises MultipleResultsFound
+
+
+get_or_none
+returns None
+raises MultipleResultsFound
+
+
+first
+returns None
+returns the first match silently
+
+
+
+Use get when the record must exist (e.g. a detail endpoint that should return 404):
+user = await UserCrud . get ( session = session , filters = [ User . id == user_id ])
+
+Use get_or_none when the record may not exist but you still want strict uniqueness enforcement:
+user = await UserCrud . get_or_none ( session = session , filters = [ User . email == email ])
+if user is None :
+ ... # handle missing case without catching an exception
+
+Use first when you only care about any one match and don't need uniqueness:
+user = await UserCrud . first ( session = session , filters = [ User . is_active == True ])
+
+
+
+
Added in v1.1 (only offset_pagination via paginate if <v1.1)
+
+Two pagination strategies are available. Both return a PaginatedResponse but differ in how they navigate through results.
+
+
+
+
+offset_paginate
+cursor_paginate
+
+
+
+
+Total count
+Yes
+No
+
+
+Jump to arbitrary page
+Yes
+No
+
+
+Performance on deep pages
+Degrades
+Constant
+
+
+Stable under concurrent inserts
+No
+Yes
+
+
+Search compatible
+Yes
+Yes
+
+
+Use case
+Admin panels, numbered pagination
+Feeds, APIs, infinite scroll
+
+
+
+
+@router . get (
+ "" ,
+ response_model = PaginatedResponse [ User ],
+)
+async def get_users (
+ session : SessionDep ,
+ items_per_page : int = 50 ,
+ page : int = 1 ,
+):
+ return await crud . UserCrud . offset_paginate (
+ session = session ,
+ items_per_page = items_per_page ,
+ page = page ,
+ )
+
+The offset_paginate method returns a PaginatedResponse whose pagination field is an OffsetPagination object:
+{
+ "status" : "SUCCESS" ,
+ "data" : [ "..." ],
+ "pagination" : {
+ "total_count" : 100 ,
+ "page" : 1 ,
+ "items_per_page" : 20 ,
+ "has_more" : true
+ }
+}
+
+
+@router . get (
+ "" ,
+ response_model = PaginatedResponse [ UserRead ],
+)
+async def list_users (
+ session : SessionDep ,
+ cursor : str | None = None ,
+ items_per_page : int = 20 ,
+):
+ return await UserCrud . cursor_paginate (
+ session = session ,
+ cursor = cursor ,
+ items_per_page = items_per_page ,
+ )
+
+The cursor_paginate method returns a PaginatedResponse whose pagination field is a CursorPagination object:
+{
+ "status" : "SUCCESS" ,
+ "data" : [ "..." ],
+ "pagination" : {
+ "next_cursor" : "eyJ2YWx1ZSI6ICIzZjQ3YWM2OS0uLi4ifQ==" ,
+ "prev_cursor" : null ,
+ "items_per_page" : 20 ,
+ "has_more" : true
+ }
+}
+
+Pass next_cursor as the cursor query parameter on the next request to advance to the next page. prev_cursor is set on pages 2+ and points back to the first item of the current page. Both are null when there is no adjacent page.
+Choosing a cursor column
+The cursor column is set once on CrudFactory via the cursor_column parameter. It must be monotonically ordered for stable results:
+
+Auto-increment integer PKs
+UUID v7 PKs
+Timestamps
+
+
+
Warning
+
Random UUID v4 PKs are not suitable as cursor columns because their ordering is non-deterministic.
+
+
+
Note
+
cursor_column is required. Calling cursor_paginate on a CRUD class that has no cursor_column configured raises a ValueError.
+
+The cursor value is base64-encoded when returned to the client and decoded back to the correct Python type on the next request. The following SQLAlchemy column types are supported:
+
+
+
+SQLAlchemy type
+Python type
+
+
+
+
+Integer, BigInteger, SmallInteger
+int
+
+
+Uuid
+uuid.UUID
+
+
+DateTime
+datetime.datetime
+
+
+Date
+datetime.date
+
+
+Float, Numeric
+decimal.Decimal
+
+
+
+# Paginate by the primary key
+PostCrud = CrudFactory ( model = Post , cursor_column = Post . id )
+
+# Paginate by a timestamp column instead
+PostCrud = CrudFactory ( model = Post , cursor_column = Post . created_at )
+
+Search
+Two search strategies are available, both compatible with offset_paginate and cursor_paginate .
+
+
+
+
+Full-text search
+Faceted search
+
+
+
+
+Input
+Free-text string
+Exact column values
+
+
+Relationship support
+Yes
+Yes
+
+
+Use case
+Search bars
+Filter dropdowns
+
+
+
+
+
You can use both search strategies in the same endpoint!
+
+Full-text search
+
+
Added in v2.2.1
+
The model's primary key is always included in searchable_fields automatically, so searching by ID works out of the box without any configuration. When no searchable_fields are declared, only the primary key is searched.
+
+Declare searchable_fields on the CRUD class. Relationship traversal is supported via tuples:
+PostCrud = CrudFactory (
+ model = Post ,
+ searchable_fields = [
+ Post . title ,
+ Post . content ,
+ ( Post . author , User . username ), # search across relationship
+ ],
+)
+
+You can override searchable_fields per call with search_fields:
+result = await UserCrud . offset_paginate (
+ session = session ,
+ search_fields = [ User . country ],
+)
+
+This allows searching with both offset_paginate and cursor_paginate :
+@router . get (
+ "" ,
+ response_model = PaginatedResponse [ User ],
+)
+async def get_users (
+ session : SessionDep ,
+ items_per_page : int = 50 ,
+ page : int = 1 ,
+ search : str | None = None ,
+):
+ return await crud . UserCrud . offset_paginate (
+ session = session ,
+ items_per_page = items_per_page ,
+ page = page ,
+ search = search ,
+ )
+
+@router . get (
+ "" ,
+ response_model = PaginatedResponse [ User ],
+)
+async def get_users (
+ session : SessionDep ,
+ cursor : str | None = None ,
+ items_per_page : int = 50 ,
+ search : str | None = None ,
+):
+ return await crud . UserCrud . cursor_paginate (
+ session = session ,
+ items_per_page = items_per_page ,
+ cursor = cursor ,
+ search = search ,
+ )
+
+Faceted search
+
+Declare facet_fields on the CRUD class to return distinct column values alongside paginated results. This is useful for populating filter dropdowns or building faceted search UIs.
+Facet fields use the same syntax as searchable_fields — direct columns or relationship tuples:
+UserCrud = CrudFactory (
+ model = User ,
+ facet_fields = [
+ User . status ,
+ User . country ,
+ ( User . role , Role . name ), # value from a related model
+ ],
+)
+
+You can override facet_fields per call:
+result = await UserCrud . offset_paginate (
+ session = session ,
+ facet_fields = [ User . country ],
+)
+
+The distinct values are returned in the filter_attributes field of PaginatedResponse :
+{
+ "status" : "SUCCESS" ,
+ "data" : [ "..." ],
+ "pagination" : { "..." },
+ "filter_attributes" : {
+ "status" : [ "active" , "inactive" ],
+ "country" : [ "DE" , "FR" , "US" ],
+ "name" : [ "admin" , "editor" , "viewer" ]
+ }
+}
+
+Use filter_by to pass the client's chosen filter values directly — no need to build SQLAlchemy conditions by hand. Any unknown key raises InvalidFacetFilterError .
+
+
The keys in filter_by are the same keys the client received in filter_attributes.
+
Keys are normally the terminal column.key (e.g. "name" for Role.name). When two facet fields share the same column key (e.g. (Build.project, Project.name) and (Build.os, Os.name)), the relationship name is prepended automatically: "project__name" and "os__name".
+
+filter_by and filters can be combined — both are applied with AND logic.
+Use filter_params() to generate a dict with the facet filter values from the query parameters:
+from typing import Annotated
+
+from fastapi import Depends
+
+UserCrud = CrudFactory (
+ model = User ,
+ facet_fields = [ User . status , User . country , ( User . role , Role . name )],
+)
+
+@router . get ( "" , response_model_exclude_none = True )
+async def list_users (
+ session : SessionDep ,
+ page : int = 1 ,
+ filter_by : Annotated [ dict [ str , list [ str ]], Depends ( UserCrud . filter_params ())],
+) -> PaginatedResponse [ UserRead ]:
+ return await UserCrud . offset_paginate (
+ session = session ,
+ page = page ,
+ filter_by = filter_by ,
+ )
+
+Both single-value and multi-value query parameters work:
+ GET /users?status=active → filter_by={"status": ["active"]}
+ GET /users?status=active&country=FR → filter_by={"status": ["active"], "country": ["FR"]}
+ GET /users?role=admin&role=editor → filter_by={"role": ["admin", "editor"]} (IN clause)
+
+Sorting
+
+Declare order_fields on the CRUD class to expose client-driven column ordering via order_by and order query parameters.
+UserCrud = CrudFactory (
+ model = User ,
+ order_fields = [
+ User . name ,
+ User . created_at ,
+ ],
+)
+
+Call order_params() to generate a FastAPI dependency that maps the query parameters to an OrderByClause expression:
+from typing import Annotated
+
+from fastapi import Depends
+from fastapi_toolsets.crud import OrderByClause
+
+@router . get ( "" )
+async def list_users (
+ session : SessionDep ,
+ order_by : Annotated [ OrderByClause | None , Depends ( UserCrud . order_params ())],
+) -> PaginatedResponse [ UserRead ]:
+ return await UserCrud . offset_paginate ( session = session , order_by = order_by )
+
+The dependency adds two query parameters to the endpoint:
+
+
+
+Parameter
+Type
+
+
+
+
+order_by
+str | null
+
+
+order
+asc or desc
+
+
+
+ GET /users?order_by=name&order=asc → ORDER BY users.name ASC
+ GET /users?order_by=name&order=desc → ORDER BY users.name DESC
+
+An unknown order_by value raises InvalidOrderFieldError (HTTP 422).
+You can also pass order_fields directly to order_params() to override the class-level defaults without modifying them:
+UserOrderParams = UserCrud . order_params ( order_fields = [ User . name ])
+
+Relationship loading
+
+By default, SQLAlchemy relationships are not loaded unless explicitly requested. Instead of using lazy="selectin" on model definitions (which is implicit and applies globally), define a default_load_options on the CRUD class to control loading strategy explicitly.
+
+
Warning
+
Avoid using lazy="selectin" on model relationships. It fires silently on every query, cannot be disabled per-call, and can cause unexpected cascading loads through deep relationship chains. Use default_load_options instead.
+
+from sqlalchemy.orm import selectinload
+
+ArticleCrud = CrudFactory (
+ model = Article ,
+ default_load_options = [
+ selectinload ( Article . category ),
+ selectinload ( Article . tags ),
+ ],
+)
+
+default_load_options applies automatically to all read operations (get, first, get_multi, offset_paginate, cursor_paginate). When load_options is passed at call-site, it fully replaces default_load_options for that query — giving you precise per-call control:
+# Only loads category, tags are not loaded
+article = await ArticleCrud . get (
+ session = session ,
+ filters = [ Article . id == article_id ],
+ load_options = [ selectinload ( Article . category )],
+)
+
+# Loads nothing — useful for write-then-refresh flows or lightweight checks
+articles = await ArticleCrud . get_multi ( session = session , load_options = [])
+
+Many-to-many relationships
+Use m2m_fields to map schema fields containing lists of IDs to SQLAlchemy relationships. The CRUD class resolves and validates all IDs before persisting:
+PostCrud = CrudFactory (
+ model = Post ,
+ m2m_fields = { "tag_ids" : Post . tags },
+)
+
+post = await PostCrud . create ( session = session , obj = PostCreateSchema ( title = "Hello" , tag_ids = [ 1 , 2 , 3 ]))
+
+Upsert
+Atomic INSERT ... ON CONFLICT DO UPDATE using PostgreSQL:
+await UserCrud . upsert (
+ session = session ,
+ obj = UserCreateSchema ( email = "alice@example.com" , username = "alice" ),
+ index_elements = [ User . email ],
+ set_ = { "username" },
+)
+
+Response serialization
+
+Pass a Pydantic schema class to create, get, update, or offset_paginate to serialize the result directly into that schema and wrap it in a Response[schema] or PaginatedResponse[schema] :
+class UserRead ( PydanticBase ):
+ id : UUID
+ username : str
+
+@router . get (
+ "/ {uuid} " ,
+ responses = generate_error_responses ( NotFoundError ),
+)
+async def get_user ( session : SessionDep , uuid : UUID ) -> Response [ UserRead ]:
+ return await crud . UserCrud . get (
+ session = session ,
+ filters = [ User . id == uuid ],
+ schema = UserRead ,
+ )
+
+@router . get ( "" )
+async def list_users ( session : SessionDep , page : int = 1 ) -> PaginatedResponse [ UserRead ]:
+ return await crud . UserCrud . offset_paginate (
+ session = session ,
+ page = page ,
+ schema = UserRead ,
+ )
+
+The schema must have from_attributes=True (or inherit from PydanticBase ) so it can be built from SQLAlchemy model instances.
+
+ API Reference
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back to top
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v2.2/module/db/index.html b/v2.2/module/db/index.html
new file mode 100644
index 0000000..9ffc136
--- /dev/null
+++ b/v2.2/module/db/index.html
@@ -0,0 +1,1800 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ DB - FastAPI Toolsets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Home
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+DB
+SQLAlchemy async session management with transactions, table locking, and row-change polling.
+
+
Info
+
This module has been coded and tested to be compatible with PostgreSQL only.
+
+Overview
+The db module provides helpers to create FastAPI dependencies and context managers for AsyncSession, along with utilities for nested transactions, table lock and polling for row changes.
+Session dependency
+Use create_db_dependency to create a FastAPI dependency that yields a session and auto-commits on success:
+from sqlalchemy.ext.asyncio import create_async_engine , async_sessionmaker
+from fastapi_toolsets.db import create_db_dependency
+
+engine = create_async_engine ( url = "postgresql+asyncpg://..." , future = True )
+session_maker = async_sessionmaker ( bind = engine , expire_on_commit = False )
+
+get_db = create_db_dependency ( session_maker = session_maker )
+
+@router . get ( "/users" )
+async def list_users ( session : AsyncSession = Depends ( get_db )):
+ ...
+
+Session context manager
+Use create_db_context for sessions outside request handlers (e.g. background tasks, CLI commands):
+from fastapi_toolsets.db import create_db_context
+
+db_context = create_db_context ( session_maker = session_maker )
+
+async def seed ():
+ async with db_context () as session :
+ ...
+
+Nested transactions
+get_transaction handles savepoints automatically, allowing safe nesting:
+from fastapi_toolsets.db import get_transaction
+
+async def create_user_with_role ( session = session ):
+ async with get_transaction ( session = session ):
+ ...
+ async with get_transaction ( session = session ): # uses savepoint
+ ...
+
+Table locking
+lock_tables acquires PostgreSQL table-level locks before executing critical sections:
+from fastapi_toolsets.db import lock_tables
+
+async with lock_tables ( session = session , tables = [ User ], mode = "EXCLUSIVE" ):
+ # No other transaction can modify User until this block exits
+ ...
+
+Available lock modes are defined in LockMode : ACCESS_SHARE, ROW_SHARE, ROW_EXCLUSIVE, SHARE_UPDATE_EXCLUSIVE, SHARE, SHARE_ROW_EXCLUSIVE, EXCLUSIVE, ACCESS_EXCLUSIVE.
+Row-change polling
+wait_for_row_change polls a row until a specific column changes value, useful for waiting on async side effects:
+from fastapi_toolsets.db import wait_for_row_change
+
+# Wait up to 30s for order.status to change
+await wait_for_row_change (
+ session = session ,
+ model = Order ,
+ pk_value = order_id ,
+ columns = [ Order . status ],
+ interval = 1.0 ,
+ timeout = 30.0 ,
+)
+
+Creating a database
+
+create_database creates a database at a given URL. It connects to server_url and issues a CREATE DATABASE statement:
+from fastapi_toolsets.db import create_database
+
+SERVER_URL = "postgresql+asyncpg://postgres:postgres@localhost/postgres"
+
+await create_database ( db_name = "myapp_test" , server_url = SERVER_URL )
+
+For test isolation with automatic cleanup, use create_worker_database from the pytest module instead — it handles drop-before, create, and drop-after automatically.
+Cleaning up tables
+
+cleanup_tables truncates all tables:
+from fastapi_toolsets.db import cleanup_tables
+
+@pytest . fixture ( autouse = True )
+async def clean ( db_session ):
+ yield
+ await cleanup_tables ( session = db_session , base = Base )
+
+
+ API Reference
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back to top
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v2.2/module/dependencies/index.html b/v2.2/module/dependencies/index.html
new file mode 100644
index 0000000..6869b76
--- /dev/null
+++ b/v2.2/module/dependencies/index.html
@@ -0,0 +1,1688 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Dependencies - FastAPI Toolsets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Home
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Dependencies
+FastAPI dependency factories for automatic model resolution from path and body parameters.
+Overview
+The dependencies module provides two factory functions that create FastAPI dependencies to fetch a model instance from the database automatically — either from a path parameter or from a request body field — and inject it directly into your route handler.
+PathDependency
+PathDependency resolves a model from a URL path parameter and injects it into the route handler. Raises NotFoundError automatically if the record does not exist.
+from fastapi_toolsets.dependencies import PathDependency
+
+UserDep = PathDependency ( model = User , field = User . id , session_dep = get_db )
+
+@router . get ( "/users/ {user_id} " )
+async def get_user ( user : User = UserDep ):
+ return user
+
+By default the parameter name is inferred from the field (user_id for User.id). You can override it:
+UserDep = PathDependency ( model = User , field = User . id , session_dep = get_db , param_name = "id" )
+
+@router . get ( "/users/ {id} " )
+async def get_user ( user : User = UserDep ):
+ return user
+
+BodyDependency
+BodyDependency resolves a model from a field in the request body. Useful when a body contains a foreign key and you want the full object injected:
+from fastapi_toolsets.dependencies import BodyDependency
+
+RoleDep = BodyDependency ( model = Role , field = Role . id , session_dep = get_db , body_field = "role_id" )
+
+@router . post ( "/users" )
+async def create_user ( body : UserCreateSchema , role : Role = RoleDep ):
+ user = User ( username = body . username , role = role )
+ ...
+
+
+ API Reference
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back to top
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v2.2/module/exceptions/index.html b/v2.2/module/exceptions/index.html
new file mode 100644
index 0000000..80a842f
--- /dev/null
+++ b/v2.2/module/exceptions/index.html
@@ -0,0 +1,1873 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Exceptions - FastAPI Toolsets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Home
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Exceptions
+Structured API exceptions with consistent error responses and automatic OpenAPI documentation.
+Overview
+The exceptions module provides a set of pre-built HTTP exceptions and a FastAPI exception handler that formats all errors — including validation errors — into a uniform ErrorResponse .
+Setup
+Register the exception handlers on your FastAPI app at startup:
+from fastapi import FastAPI
+from fastapi_toolsets.exceptions import init_exceptions_handlers
+
+app = FastAPI ()
+init_exceptions_handlers ( app = app )
+
+This registers handlers for:
+
+ApiException — all custom exceptions below
+HTTPException — Starlette/FastAPI HTTP errors
+RequestValidationError — Pydantic request validation (422)
+ResponseValidationError — Pydantic response validation (422)
+Exception — unhandled errors (500)
+
+It also patches app.openapi() to replace the default Pydantic 422 schema with a structured example matching the ErrorResponse format.
+Built-in exceptions
+
+Per-instance overrides
+All built-in exceptions accept optional keyword arguments to customise the response for a specific raise site without changing the class defaults:
+
+
+
+Argument
+Effect
+
+
+
+
+detail
+Overrides both str(exc) (log output) and the message field in the response body
+
+
+desc
+Overrides the description field
+
+
+data
+Overrides the data field
+
+
+
+raise NotFoundError ( detail = "User 42 not found" , desc = "No user with that ID exists in the database." )
+
+Custom exceptions
+Subclass ApiException and define an api_error class variable:
+from fastapi_toolsets.exceptions import ApiException
+from fastapi_toolsets.schemas import ApiError
+
+class PaymentRequiredError ( ApiException ):
+ api_error = ApiError (
+ code = 402 ,
+ msg = "Payment Required" ,
+ desc = "Your subscription has expired." ,
+ err_code = "BILLING-402" ,
+ )
+
+
+
Warning
+
Subclasses that do not define api_error raise a TypeError at class creation time , not at raise time.
+
+Custom __init__
+Override __init__ to compute detail, desc, or data dynamically, then delegate to super().__init__():
+class OrderValidationError ( ApiException ):
+ api_error = ApiError (
+ code = 422 ,
+ msg = "Order Validation Failed" ,
+ desc = "One or more order fields are invalid." ,
+ err_code = "ORDER-422" ,
+ )
+
+ def __init__ ( self , * field_errors : str ) -> None :
+ super () . __init__ (
+ f " { len ( field_errors ) } validation error(s)" ,
+ desc = ", " . join ( field_errors ),
+ data = { "errors" : [{ "message" : e } for e in field_errors ]},
+ )
+
+
+Use abstract=True when creating a shared base that is not meant to be raised directly:
+class BillingError ( ApiException , abstract = True ):
+ """Base for all billing-related errors."""
+
+class PaymentRequiredError ( BillingError ):
+ api_error = ApiError ( code = 402 , msg = "Payment Required" , desc = "..." , err_code = "BILLING-402" )
+
+class SubscriptionExpiredError ( BillingError ):
+ api_error = ApiError ( code = 402 , msg = "Subscription Expired" , desc = "..." , err_code = "BILLING-402-EXP" )
+
+OpenAPI response documentation
+Use generate_error_responses to add error schemas to your endpoint's OpenAPI spec:
+from fastapi_toolsets.exceptions import generate_error_responses , NotFoundError , ForbiddenError
+
+@router . get (
+ "/users/ {id} " ,
+ responses = generate_error_responses ( NotFoundError , ForbiddenError ),
+)
+async def get_user ( ... ): ...
+
+Multiple exceptions sharing the same HTTP status code are grouped under one entry, each appearing as a named example keyed by its err_code. This keeps the OpenAPI UI readable when several error variants map to the same status.
+
+ API Reference
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back to top
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v2.2/module/fixtures/index.html b/v2.2/module/fixtures/index.html
new file mode 100644
index 0000000..5263240
--- /dev/null
+++ b/v2.2/module/fixtures/index.html
@@ -0,0 +1,1822 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Fixtures - FastAPI Toolsets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Home
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Fixtures
+Dependency-aware database seeding with context-based loading strategies.
+Overview
+The fixtures module lets you define named fixtures with dependencies between them, then load them into the database in the correct order. Fixtures can be scoped to contexts (e.g. base data, testing data) so that only the relevant ones are loaded for each environment.
+Defining fixtures
+from fastapi_toolsets.fixtures import FixtureRegistry , Context
+
+fixtures = FixtureRegistry ()
+
+@fixtures . register
+def roles ():
+ return [
+ Role ( id = 1 , name = "admin" ),
+ Role ( id = 2 , name = "user" ),
+ ]
+
+@fixtures . register ( depends_on = [ "roles" ], contexts = [ Context . TESTING ])
+def test_users ():
+ return [
+ User ( id = 1 , username = "alice" , role_id = 1 ),
+ User ( id = 2 , username = "bob" , role_id = 2 ),
+ ]
+
+Dependencies declared via depends_on are resolved topologically — roles will always be loaded before test_users.
+Loading fixtures
+By context with load_fixtures_by_context :
+from fastapi_toolsets.fixtures import load_fixtures_by_context
+
+async with db_context () as session :
+ await load_fixtures_by_context ( session = session , registry = fixtures , context = Context . TESTING )
+
+Directly with load_fixtures :
+from fastapi_toolsets.fixtures import load_fixtures
+
+async with db_context () as session :
+ await load_fixtures ( session = session , registry = fixtures )
+
+Contexts
+Context is an enum with predefined values:
+
+
+
+Context
+Description
+
+
+
+
+Context.BASE
+Core data required in all environments
+
+
+Context.TESTING
+Data only loaded during tests
+
+
+Context.PRODUCTION
+Data only loaded in production
+
+
+
+A fixture with no contexts defined takes Context.BASE by default.
+Load strategies
+LoadStrategy controls how the fixture loader handles rows that already exist:
+
+
+
+Strategy
+Description
+
+
+
+
+LoadStrategy.INSERT
+Insert only, fail on duplicates
+
+
+LoadStrategy.UPSERT
+Insert or update on conflict
+
+
+LoadStrategy.SKIP
+Skip rows that already exist
+
+
+
+Merging registries
+Split fixtures definitions across modules and merge them:
+from myapp.fixtures.dev import dev_fixtures
+from myapp.fixtures.prod import prod_fixtures
+
+fixtures = fixturesRegistry ()
+fixtures . include_registry ( registry = dev_fixtures )
+fixtures . include_registry ( registry = prod_fixtures )
+
+## Pytest integration
+
+Use [ ` register_fixtures ` ]( ../ reference / pytest . md #fastapi_toolsets.pytest.plugin.register_fixtures) to expose each fixture in your registry as an injectable pytest fixture named `fixture_{name}` by default:
+
+``` python
+# conftest.py
+import pytest
+from fastapi_toolsets.pytest import create_db_session , register_fixtures
+from app.fixtures import registry
+from app.models import Base
+
+DATABASE_URL = "postgresql+asyncpg://user:pass@localhost/test_db"
+
+@pytest . fixture
+async def db_session ():
+ async with create_db_session ( database_url = DATABASE_URL , base = Base , cleanup = True ) as session :
+ yield session
+
+register_fixtures ( registry = registry , namespace = globals ())
+
+# test_users.py
+async def test_user_can_login ( fixture_users : list [ User ], fixture_roles : list [ Role ]):
+ ...
+
+The load order is resolved automatically from the depends_on declarations in your registry. Each generated fixture receives db_session as a dependency and returns the list of loaded model instances.
+CLI integration
+Fixtures can be triggered from the CLI. See the CLI module for setup instructions.
+
+ API Reference
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back to top
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v2.2/module/logger/index.html b/v2.2/module/logger/index.html
new file mode 100644
index 0000000..1e81a79
--- /dev/null
+++ b/v2.2/module/logger/index.html
@@ -0,0 +1,1677 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Logger - FastAPI Toolsets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Home
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Logger
+Lightweight logging utilities with consistent formatting and uvicorn integration.
+Overview
+The logger module provides two helpers: one to configure the root logger (and uvicorn loggers) at startup, and one to retrieve a named logger anywhere in your codebase.
+Setup
+Call configure_logging once at application startup:
+from fastapi_toolsets.logger import configure_logging
+
+configure_logging ( level = "INFO" )
+
+This sets up a stdout handler with a consistent format and also configures uvicorn's access and error loggers so all log output shares the same style.
+Getting a logger
+from fastapi_toolsets.logger import get_logger
+
+logger = get_logger ( name = __name__ )
+logger . info ( "User created" )
+
+When called without arguments, get_logger auto-detects the caller's module name via frame inspection:
+# Equivalent to get_logger(name=__name__)
+logger = get_logger ()
+
+
+ API Reference
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back to top
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v2.2/module/metrics/index.html b/v2.2/module/metrics/index.html
new file mode 100644
index 0000000..6bfff70
--- /dev/null
+++ b/v2.2/module/metrics/index.html
@@ -0,0 +1,1797 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Metrics - FastAPI Toolsets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Home
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Metrics
+Prometheus metrics integration with a decorator-based registry and multi-process support.
+Installation
+uv pip
+
+
+
uv add "fastapi-toolsets[metrics]"
+
+
+
+
pip install "fastapi-toolsets[metrics]"
+
+
+
+
+Overview
+The metrics module provides a MetricsRegistry to declare Prometheus metrics with decorators, and an init_metrics function to mount a /metrics endpoint on your FastAPI app.
+Setup
+from fastapi import FastAPI
+from fastapi_toolsets.metrics import MetricsRegistry , init_metrics
+
+app = FastAPI ()
+metrics = MetricsRegistry ()
+
+init_metrics ( app = app , registry = metrics )
+
+This mounts the /metrics endpoint that Prometheus can scrape.
+Declaring metrics
+Providers
+Providers are called once at startup by init_metrics. The return value (the Prometheus metric object) is stored in the registry and can be retrieved later with registry.get(name) .
+Use providers when you want deferred initialization : the Prometheus metric is not registered with the global CollectorRegistry until init_metrics runs, not at import time. This is particularly useful for testing — importing the module in a test suite without calling init_metrics leaves no metrics registered, avoiding cross-test pollution.
+It is also useful when metrics are defined across multiple modules and merged with include_registry: any code that needs a metric can call metrics.get() on the shared registry instead of importing the metric directly from its origin module.
+If neither of these applies to you, declaring metrics at module level (e.g. HTTP_REQUESTS = Counter(...)) is simpler and equally valid.
+from prometheus_client import Counter , Histogram
+
+@metrics . register
+def http_requests ():
+ return Counter ( "http_requests_total" , "Total HTTP requests" , [ "method" , "status" ])
+
+@metrics . register
+def request_duration ():
+ return Histogram ( "request_duration_seconds" , "Request duration" )
+
+To use a provider's metric elsewhere (e.g. in a middleware), call metrics.get() inside the handler — not at module level, as providers are only initialized when init_metrics runs:
+async def metrics_middleware ( request : Request , call_next ):
+ response = await call_next ( request )
+ metrics . get ( "http_requests" ) . labels (
+ method = request . method , status = response . status_code
+ ) . inc ()
+ return response
+
+Collectors
+Collectors are called on every scrape. Use them for metrics that reflect current state (e.g. gauges).
+
+
Declare the metric at module level
+
Do not instantiate the Prometheus metric inside the collector function. Doing so recreates it on every scrape, raising ValueError: Duplicated timeseries in CollectorRegistry. Declare it once at module level instead:
+
+from prometheus_client import Gauge
+
+_queue_depth = Gauge ( "queue_depth" , "Current queue depth" )
+
+@metrics . register ( collect = True )
+def collect_queue_depth ():
+ _queue_depth . set ( get_current_queue_depth ())
+
+Merging registries
+Split metrics definitions across modules and merge them:
+from myapp.metrics.http import http_metrics
+from myapp.metrics.db import db_metrics
+
+metrics = MetricsRegistry ()
+metrics . include_registry ( registry = http_metrics )
+metrics . include_registry ( registry = db_metrics )
+
+Multi-process mode
+Multi-process support is enabled automatically when the PROMETHEUS_MULTIPROC_DIR environment variable is set. No code changes are required.
+
+
Environment variable name
+
The correct variable is PROMETHEUS_MULTIPROC_DIR (not PROMETHEUS_MULTIPROCESS_DIR).
+
+
+ API Reference
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back to top
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v2.2/module/models/index.html b/v2.2/module/models/index.html
new file mode 100644
index 0000000..1619212
--- /dev/null
+++ b/v2.2/module/models/index.html
@@ -0,0 +1,1790 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Models - FastAPI Toolsets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Home
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Models
+
+Reusable SQLAlchemy 2.0 mixins for common column patterns, designed to be composed freely on any DeclarativeBase model.
+Overview
+The models module provides mixins that each add a single, well-defined column behaviour. They work with standard SQLAlchemy 2.0 declarative syntax and are fully compatible with AsyncSession.
+from fastapi_toolsets.models import UUIDMixin , TimestampMixin
+
+class Article ( Base , UUIDMixin , TimestampMixin ):
+ __tablename__ = "articles"
+
+ title : Mapped [ str ]
+ content : Mapped [ str ]
+
+All timestamp columns are timezone-aware (TIMESTAMPTZ). All defaults are server-side, so they are also applied when inserting rows via raw SQL outside the ORM.
+Mixins
+
+Adds a id: UUID primary key generated server-side by PostgreSQL using gen_random_uuid() (requires PostgreSQL 13+). The value is retrieved via RETURNING after insert, so it is available on the Python object immediately after flush().
+from fastapi_toolsets.models import UUIDMixin
+
+class User ( Base , UUIDMixin ):
+ __tablename__ = "users"
+
+ username : Mapped [ str ]
+
+# id is None before flush
+user = User ( username = "alice" )
+await session . flush ()
+print ( user . id ) # UUID('...')
+
+
+Adds a created_at: datetime column set to NOW() on insert. The column has no onupdate hook — it is intentionally immutable after the row is created.
+from fastapi_toolsets.models import UUIDMixin , CreatedAtMixin
+
+class Order ( Base , UUIDMixin , CreatedAtMixin ):
+ __tablename__ = "orders"
+
+ total : Mapped [ float ]
+
+
+Adds an updated_at: datetime column set to NOW() on insert and automatically updated to NOW() on every ORM-level update (via SQLAlchemy's onupdate hook).
+from fastapi_toolsets.models import UUIDMixin , UpdatedAtMixin
+
+class Post ( Base , UUIDMixin , UpdatedAtMixin ):
+ __tablename__ = "posts"
+
+ title : Mapped [ str ]
+
+post = Post ( title = "Hello" )
+await session . flush ()
+await session . refresh ( post )
+
+post . title = "Hello World"
+await session . flush ()
+await session . refresh ( post )
+print ( post . updated_at )
+
+
+
Note
+
updated_at is updated by SQLAlchemy at ORM flush time. If you update rows via raw SQL (e.g. UPDATE posts SET ...), the column will not be updated automatically — use a database trigger if you need that guarantee.
+
+
+Convenience mixin that combines CreatedAtMixin and UpdatedAtMixin . Equivalent to inheriting both.
+from fastapi_toolsets.models import UUIDMixin , TimestampMixin
+
+class Article ( Base , UUIDMixin , TimestampMixin ):
+ __tablename__ = "articles"
+
+ title : Mapped [ str ]
+
+Composing mixins
+All mixins can be combined in any order. The only constraint is that exactly one primary key must be defined — either via UUIDMixin or directly on the model.
+from fastapi_toolsets.models import UUIDMixin , TimestampMixin
+
+class Event ( Base , UUIDMixin , TimestampMixin ):
+ __tablename__ = "events"
+ name : Mapped [ str ]
+
+class Counter ( Base , UpdatedAtMixin ):
+ __tablename__ = "counters"
+ id : Mapped [ int ] = mapped_column ( primary_key = True , autoincrement = True )
+ value : Mapped [ int ]
+
+
+ API Reference
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back to top
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v2.2/module/pytest/index.html b/v2.2/module/pytest/index.html
new file mode 100644
index 0000000..8b091c3
--- /dev/null
+++ b/v2.2/module/pytest/index.html
@@ -0,0 +1,1766 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Pytest - FastAPI Toolsets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Home
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Pytest
+Testing helpers for FastAPI applications with async client, database sessions, and parallel worker support.
+Installation
+uv pip
+
+
+
uv add "fastapi-toolsets[pytest]"
+
+
+
+
pip install "fastapi-toolsets[pytest]"
+
+
+
+
+Overview
+The pytest module provides utilities for setting up async test clients, managing test database sessions, and supporting parallel test execution with pytest-xdist.
+Creating an async client
+Use create_async_client to get an httpx.AsyncClient configured for your FastAPI app:
+from fastapi_toolsets.pytest import create_async_client
+
+@pytest . fixture
+async def http_client ( db_session ):
+ async def _override_get_db ():
+ yield db_session
+
+ async with create_async_client (
+ app = app ,
+ base_url = "http://127.0.0.1/api/v1" ,
+ dependency_overrides = { get_db : _override_get_db },
+ ) as c :
+ yield c
+
+Database sessions in tests
+Use create_db_session to create an isolated AsyncSession for a test, combined with create_worker_database to set up a per-worker database:
+from fastapi_toolsets.pytest import create_worker_database , create_db_session
+
+@pytest . fixture ( scope = "session" )
+async def worker_db_url ():
+ async with create_worker_database (
+ database_url = str ( settings . SQLALCHEMY_DATABASE_URI )
+ ) as url :
+ yield url
+
+
+@pytest . fixture
+async def db_session ( worker_db_url ):
+ async with create_db_session (
+ database_url = worker_db_url , base = Base , cleanup = True
+ ) as session :
+ yield session
+
+
+
Info
+
In this example, the database is reset between each test using the argument cleanup=True.
+
+Use worker_database_url to derive the per-worker URL manually if needed:
+from fastapi_toolsets.pytest import worker_database_url
+
+url = worker_database_url ( "postgresql+asyncpg://user:pass@localhost/test_db" , default_test_db = "test" )
+# e.g. "postgresql+asyncpg://user:pass@localhost/test_db_gw0" under xdist
+
+Parallel testing with pytest-xdist
+The examples above are already compatible with parallel test execution with pytest-xdist.
+Cleaning up tables
+
+
Warning
+
Since V2.1.0 cleanup_tables now live in fastapi_toolsets.db. For backward compatibility the function is still available in fastapi_toolsets.pytest, but this will be remove in V3.0.0.
+
+If you want to manually clean up a database you can use cleanup_tables , this will truncate all tables between tests for fast isolation:
+from fastapi_toolsets.db import cleanup_tables
+
+@pytest . fixture ( autouse = True )
+async def clean ( db_session ):
+ yield
+ await cleanup_tables ( session = db_session , base = Base )
+
+
+ API Reference
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back to top
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v2.2/module/schemas/index.html b/v2.2/module/schemas/index.html
new file mode 100644
index 0000000..f838f1a
--- /dev/null
+++ b/v2.2/module/schemas/index.html
@@ -0,0 +1,1762 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Schemas - FastAPI Toolsets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Home
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Modules
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Schemas
+Standardized Pydantic response models for consistent API responses across your FastAPI application.
+Overview
+The schemas module provides generic response wrappers that enforce a uniform response structure. All models use from_attributes=True for ORM compatibility and validate_assignment=True for runtime type safety.
+Response models
+
+The most common wrapper for a single resource response.
+from fastapi_toolsets.schemas import Response
+
+@router . get ( "/users/ {id} " )
+async def get_user ( user : User = UserDep ) -> Response [ UserSchema ]:
+ return Response ( data = user , message = "User retrieved" )
+
+
+Wraps a list of items with pagination metadata and optional facet values. The pagination field accepts either OffsetPagination or CursorPagination depending on the strategy used.
+
+Page-number based. Requires total_count so clients can compute the total number of pages.
+from fastapi_toolsets.schemas import PaginatedResponse , OffsetPagination
+
+@router . get ( "/users" )
+async def list_users () -> PaginatedResponse [ UserSchema ]:
+ return PaginatedResponse (
+ data = users ,
+ pagination = OffsetPagination (
+ total_count = 100 ,
+ items_per_page = 10 ,
+ page = 1 ,
+ has_more = True ,
+ ),
+ )
+
+
+Cursor based. Efficient for large or frequently updated datasets where offset pagination is impractical. Provides opaque next_cursor / prev_cursor tokens; no total count is exposed.
+from fastapi_toolsets.schemas import PaginatedResponse , CursorPagination
+
+@router . get ( "/events" )
+async def list_events () -> PaginatedResponse [ EventSchema ]:
+ return PaginatedResponse (
+ data = events ,
+ pagination = CursorPagination (
+ next_cursor = "eyJpZCI6IDQyfQ==" ,
+ prev_cursor = None ,
+ items_per_page = 20 ,
+ has_more = True ,
+ ),
+ )
+
+The optional filter_attributes field is populated when facet_fields are configured on the CRUD class (see Filter attributes ). It is None by default and can be hidden from API responses with response_model_exclude_none=True.
+
+Returned automatically by the exceptions handler.
+
+ API Reference
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back to top
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v2.2/objects.inv b/v2.2/objects.inv
new file mode 100644
index 0000000..05e6cd6
Binary files /dev/null and b/v2.2/objects.inv differ
diff --git a/v2.2/overrides/main.html b/v2.2/overrides/main.html
new file mode 100644
index 0000000..494fda9
--- /dev/null
+++ b/v2.2/overrides/main.html
@@ -0,0 +1,12 @@
+{% extends "base.html" %} {% block extrahead %}
+
+{{ super() }} {% endblock %} {% block outdated %} You're not viewing the latest
+version.
+
+ Click here to go to latest.
+
+{% endblock %}
diff --git a/v2.2/reference/cli/index.html b/v2.2/reference/cli/index.html
new file mode 100644
index 0000000..cd45e4b
--- /dev/null
+++ b/v2.2/reference/cli/index.html
@@ -0,0 +1,1993 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ cli - FastAPI Toolsets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Home
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Reference
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+cli
+Here's the reference for the CLI configuration helpers used to load settings from pyproject.toml.
+You can import them directly from fastapi_toolsets.cli.config:
+from fastapi_toolsets.cli.config import (
+ import_from_string ,
+ get_config_value ,
+ get_fixtures_registry ,
+ get_db_context ,
+ get_custom_cli ,
+)
+
+
+
+
+
+
+
+
+
+
+
+
Import an object from a dotted string path.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ import_path
+
+
+ str
+
+
+
+
Import path in "module.submodule:attribute" format
+
+
+
+ required
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ Any
+
+
+
+
The imported attribute
+
+
+
+
+
+
+
+
Raises:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ BadParameter
+
+
+
+
If the import path is invalid or import fails
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
get_config_value ( key : str , required : Literal [ True ]) -> Any
+get_config_value (
+ key : str , required : bool = False
+) -> Any | None
+
+
+
+
+
+
Get a configuration value from pyproject.toml.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ key
+
+
+ str
+
+
+
+
The configuration key in [tool.fastapi-toolsets].
+
+
+
+ required
+
+
+
+
+ required
+
+
+ bool
+
+
+
+
If True, raises an error when the key is missing.
+
+
+
+ False
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ Any | None
+
+
+
+
The configuration value, or None if not found and not required.
+
+
+
+
+
+
+
+
Raises:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ BadParameter
+
+
+
+
If required=True and the key is missing.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Import and return the fixtures registry from config.
+
+
+
+
+
+
+
+
+
+
+ fastapi_toolsets . cli . config . get_db_context ()
+
+
+
+
+
+
+
Import and return the db_context function from config.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Import and return the custom CLI Typer instance from config.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Decorator to run an async function as a sync CLI command.
+
+
+
+ Example
+ @fixture_cli . command ( "load" )
+@async_command
+async def load ( ctx : typer . Context ) -> None :
+ async with get_db_context () as session :
+ await load_fixtures ( session , registry )
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back to top
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v2.2/reference/crud/index.html b/v2.2/reference/crud/index.html
new file mode 100644
index 0000000..b273956
--- /dev/null
+++ b/v2.2/reference/crud/index.html
@@ -0,0 +1,5007 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ crud - FastAPI Toolsets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Home
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Reference
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+crud
+Here's the reference for the CRUD classes, factory, and search utilities.
+You can import the main symbols from fastapi_toolsets.crud:
+from fastapi_toolsets.crud import CrudFactory , AsyncCrud
+from fastapi_toolsets.crud.search import SearchConfig , get_searchable_fields , build_search_filters
+
+
+
+
+
+
+
+
+
+
+
+
+ Bases: Generic [ModelType ]
+
+
+
+
Generic async CRUD operations for SQLAlchemy models.
+
Subclass this and set the model class variable, or use CrudFactory.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Count records matching the filters.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ session
+
+
+ AsyncSession
+
+
+
+
+
+ required
+
+
+
+
+ filters
+
+
+ list [Any ] | None
+
+
+
+
List of SQLAlchemy filter conditions
+
+
+
+ None
+
+
+
+
+ joins
+
+
+ JoinType | None
+
+
+
+
List of (model, condition) tuples for joining related tables
+
+
+
+ None
+
+
+
+
+ outer_join
+
+
+ bool
+
+
+
+
Use LEFT OUTER JOIN instead of INNER JOIN
+
+
+
+ False
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ int
+
+
+
+
Number of matching records
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
create (
+ session : AsyncSession ,
+ obj : BaseModel ,
+ * ,
+ schema : type [ SchemaType ],
+) -> Response [ SchemaType ]
+create (
+ session : AsyncSession ,
+ obj : BaseModel ,
+ * ,
+ schema : None = ... ,
+) -> ModelType
+
+
+
+
+
+
Create a new record in the database.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ session
+
+
+ AsyncSession
+
+
+
+
+
+ required
+
+
+
+
+ obj
+
+
+ BaseModel
+
+
+
+
Pydantic model with data to create
+
+
+
+ required
+
+
+
+
+ schema
+
+
+ type [BaseModel ] | None
+
+
+
+
Pydantic schema to serialize the result into. When provided,
+the result is automatically wrapped in a Response[schema].
+
+
+
+ None
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ ModelType | Response [Any ]
+
+
+
+
Created model instance, or Response[schema] when schema is given.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Get paginated results using cursor-based pagination.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ session
+
+
+ AsyncSession
+
+
+
+
+
+ required
+
+
+
+
+ cursor
+
+
+ str | None
+
+
+
+
Cursor string from a previous CursorPagination.
+Omit (or pass None) to start from the beginning.
+
+
+
+ None
+
+
+
+
+ filters
+
+
+ list [Any ] | None
+
+
+
+
List of SQLAlchemy filter conditions.
+
+
+
+ None
+
+
+
+
+ joins
+
+
+ JoinType | None
+
+
+
+
List of (model, condition) tuples for joining related
+tables.
+
+
+
+ None
+
+
+
+
+ outer_join
+
+
+ bool
+
+
+
+
Use LEFT OUTER JOIN instead of INNER JOIN.
+
+
+
+ False
+
+
+
+
+ load_options
+
+
+ Sequence [ExecutableOption ] | None
+
+
+
+
SQLAlchemy loader options. Falls back to
+default_load_options when not provided.
+
+
+
+ None
+
+
+
+
+ order_by
+
+
+ OrderByClause | None
+
+
+
+
Additional ordering applied after the cursor column.
+
+
+
+ None
+
+
+
+
+ items_per_page
+
+
+ int
+
+
+
+
Number of items per page (default 20).
+
+
+
+ 20
+
+
+
+
+ search
+
+
+ str | SearchConfig | None
+
+
+
+
Search query string or SearchConfig object.
+
+
+
+ None
+
+
+
+
+ search_fields
+
+
+ Sequence [SearchFieldType ] | None
+
+
+
+
Fields to search in (overrides class default).
+
+
+
+ None
+
+
+
+
+ facet_fields
+
+
+ Sequence [FacetFieldType ] | None
+
+
+
+
Columns to compute distinct values for (overrides class default).
+
+
+
+ None
+
+
+
+
+ filter_by
+
+
+ dict [str , Any ] | BaseModel | None
+
+
+
+
Dict of {column_key: value} to filter by declared facet fields.
+Keys must match the column.key of a facet field. Scalar → equality,
+list → IN clause. Raises InvalidFacetFilterError for unknown keys.
+
+
+
+ None
+
+
+
+
+ schema
+
+
+ type [BaseModel ]
+
+
+
+
Optional Pydantic schema to serialize each item into.
+
+
+
+ required
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ PaginatedResponse [Any ]
+
+
+
+
PaginatedResponse with CursorPagination metadata
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
delete (
+ session : AsyncSession ,
+ filters : list [ Any ],
+ * ,
+ return_response : Literal [ True ],
+) -> Response [ None ]
+delete (
+ session : AsyncSession ,
+ filters : list [ Any ],
+ * ,
+ return_response : Literal [ False ] = ... ,
+) -> None
+
+
+
+
+
+
Delete records from the database.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ session
+
+
+ AsyncSession
+
+
+
+
+
+ required
+
+
+
+
+ filters
+
+
+ list [Any ]
+
+
+
+
List of SQLAlchemy filter conditions
+
+
+
+ required
+
+
+
+
+ return_response
+
+
+ bool
+
+
+
+
When True, returns Response[None] instead
+of None. Useful for API endpoints that expect a consistent
+response envelope.
+
+
+
+ False
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ None | Response [None]
+
+
+
+
None, or Response[None] when return_response=True.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Check if a record exists.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ session
+
+
+ AsyncSession
+
+
+
+
+
+ required
+
+
+
+
+ filters
+
+
+ list [Any ]
+
+
+
+
List of SQLAlchemy filter conditions
+
+
+
+ required
+
+
+
+
+ joins
+
+
+ JoinType | None
+
+
+
+
List of (model, condition) tuples for joining related tables
+
+
+
+ None
+
+
+
+
+ outer_join
+
+
+ bool
+
+
+
+
Use LEFT OUTER JOIN instead of INNER JOIN
+
+
+
+ False
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ bool
+
+
+
+
True if at least one record matches
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Return a FastAPI dependency that collects facet filter values from query parameters.
+Args:
+ facet_fields: Override the facet fields for this dependency. Falls back to the
+ class-level facet_fields if not provided.
+
+
+
Returns:
+
+
+
+
Raises:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ ValueError
+
+
+
+
If no facet fields are configured on this CRUD class and none are
+provided via facet_fields.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
first (
+ session : AsyncSession ,
+ filters : list [ Any ] | None = None ,
+ * ,
+ joins : JoinType | None = None ,
+ outer_join : bool = False ,
+ with_for_update : bool = False ,
+ load_options : Sequence [ ExecutableOption ] | None = None ,
+ schema : type [ SchemaType ],
+) -> Response [ SchemaType ] | None
+first (
+ session : AsyncSession ,
+ filters : list [ Any ] | None = None ,
+ * ,
+ joins : JoinType | None = None ,
+ outer_join : bool = False ,
+ with_for_update : bool = False ,
+ load_options : Sequence [ ExecutableOption ] | None = None ,
+ schema : None = ... ,
+) -> ModelType | None
+
+
+
+
+
+
Get the first matching record, or None.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ session
+
+
+ AsyncSession
+
+
+
+
+
+ required
+
+
+
+
+ filters
+
+
+ list [Any ] | None
+
+
+
+
List of SQLAlchemy filter conditions
+
+
+
+ None
+
+
+
+
+ joins
+
+
+ JoinType | None
+
+
+
+
List of (model, condition) tuples for joining related tables
+
+
+
+ None
+
+
+
+
+ outer_join
+
+
+ bool
+
+
+
+
Use LEFT OUTER JOIN instead of INNER JOIN
+
+
+
+ False
+
+
+
+
+ with_for_update
+
+
+ bool
+
+
+
+
Lock the row for update
+
+
+
+ False
+
+
+
+
+ load_options
+
+
+ Sequence [ExecutableOption ] | None
+
+
+
+
SQLAlchemy loader options (e.g., selectinload)
+
+
+
+ None
+
+
+
+
+ schema
+
+
+ type [BaseModel ] | None
+
+
+
+
Pydantic schema to serialize the result into. When provided,
+the result is automatically wrapped in a Response[schema].
+
+
+
+ None
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ ModelType | Response [Any ] | None
+
+
+
+
Model instance, Response[schema] when schema is given,
+
+
+
+
+
+ ModelType | Response [Any ] | None
+
+
+
+
or None when no record matches.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
get (
+ session : AsyncSession ,
+ filters : list [ Any ],
+ * ,
+ joins : JoinType | None = None ,
+ outer_join : bool = False ,
+ with_for_update : bool = False ,
+ load_options : Sequence [ ExecutableOption ] | None = None ,
+ schema : type [ SchemaType ],
+) -> Response [ SchemaType ]
+get (
+ session : AsyncSession ,
+ filters : list [ Any ],
+ * ,
+ joins : JoinType | None = None ,
+ outer_join : bool = False ,
+ with_for_update : bool = False ,
+ load_options : Sequence [ ExecutableOption ] | None = None ,
+ schema : None = ... ,
+) -> ModelType
+
+
+
+
+
+
Get exactly one record. Raises NotFoundError if not found.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ session
+
+
+ AsyncSession
+
+
+
+
+
+ required
+
+
+
+
+ filters
+
+
+ list [Any ]
+
+
+
+
List of SQLAlchemy filter conditions
+
+
+
+ required
+
+
+
+
+ joins
+
+
+ JoinType | None
+
+
+
+
List of (model, condition) tuples for joining related tables
+
+
+
+ None
+
+
+
+
+ outer_join
+
+
+ bool
+
+
+
+
Use LEFT OUTER JOIN instead of INNER JOIN
+
+
+
+ False
+
+
+
+
+ with_for_update
+
+
+ bool
+
+
+
+
Lock the row for update
+
+
+
+ False
+
+
+
+
+ load_options
+
+
+ Sequence [ExecutableOption ] | None
+
+
+
+
SQLAlchemy loader options (e.g., selectinload)
+
+
+
+ None
+
+
+
+
+ schema
+
+
+ type [BaseModel ] | None
+
+
+
+
Pydantic schema to serialize the result into. When provided,
+the result is automatically wrapped in a Response[schema].
+
+
+
+ None
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ ModelType | Response [Any ]
+
+
+
+
Model instance, or Response[schema] when schema is given.
+
+
+
+
+
+
+
+
Raises:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ NotFoundError
+
+
+
+
+
+
+
+ MultipleResultsFound
+
+
+
+
If more than one record found
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Get multiple records from the database.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ session
+
+
+ AsyncSession
+
+
+
+
+
+ required
+
+
+
+
+ filters
+
+
+ list [Any ] | None
+
+
+
+
List of SQLAlchemy filter conditions
+
+
+
+ None
+
+
+
+
+ joins
+
+
+ JoinType | None
+
+
+
+
List of (model, condition) tuples for joining related tables
+
+
+
+ None
+
+
+
+
+ outer_join
+
+
+ bool
+
+
+
+
Use LEFT OUTER JOIN instead of INNER JOIN
+
+
+
+ False
+
+
+
+
+ load_options
+
+
+ Sequence [ExecutableOption ] | None
+
+
+
+
SQLAlchemy loader options
+
+
+
+ None
+
+
+
+
+ order_by
+
+
+ OrderByClause | None
+
+
+
+
Column or list of columns to order by
+
+
+
+ None
+
+
+
+
+ limit
+
+
+ int | None
+
+
+
+
Max number of rows to return
+
+
+
+ None
+
+
+
+
+ offset
+
+
+ int | None
+
+
+
+
+
+ None
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ Sequence [ModelType ]
+
+
+
+
List of model instances
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
get_or_none (
+ session : AsyncSession ,
+ filters : list [ Any ],
+ * ,
+ joins : JoinType | None = None ,
+ outer_join : bool = False ,
+ with_for_update : bool = False ,
+ load_options : Sequence [ ExecutableOption ] | None = None ,
+ schema : type [ SchemaType ],
+) -> Response [ SchemaType ] | None
+get_or_none (
+ session : AsyncSession ,
+ filters : list [ Any ],
+ * ,
+ joins : JoinType | None = None ,
+ outer_join : bool = False ,
+ with_for_update : bool = False ,
+ load_options : Sequence [ ExecutableOption ] | None = None ,
+ schema : None = ... ,
+) -> ModelType | None
+
+
+
+
+
+
Get exactly one record, or None if not found.
+
Like :meth:get but returns None instead of raising
+:class:~fastapi_toolsets.exceptions.NotFoundError when no record
+matches the filters.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ session
+
+
+ AsyncSession
+
+
+
+
+
+ required
+
+
+
+
+ filters
+
+
+ list [Any ]
+
+
+
+
List of SQLAlchemy filter conditions
+
+
+
+ required
+
+
+
+
+ joins
+
+
+ JoinType | None
+
+
+
+
List of (model, condition) tuples for joining related tables
+
+
+
+ None
+
+
+
+
+ outer_join
+
+
+ bool
+
+
+
+
Use LEFT OUTER JOIN instead of INNER JOIN
+
+
+
+ False
+
+
+
+
+ with_for_update
+
+
+ bool
+
+
+
+
Lock the row for update
+
+
+
+ False
+
+
+
+
+ load_options
+
+
+ Sequence [ExecutableOption ] | None
+
+
+
+
SQLAlchemy loader options (e.g., selectinload)
+
+
+
+ None
+
+
+
+
+ schema
+
+
+ type [BaseModel ] | None
+
+
+
+
Pydantic schema to serialize the result into. When provided,
+the result is automatically wrapped in a Response[schema].
+
+
+
+ None
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ ModelType | Response [Any ] | None
+
+
+
+
Model instance, Response[schema] when schema is given,
+
+
+
+
+
+ ModelType | Response [Any ] | None
+
+
+
+
or None when no record matches.
+
+
+
+
+
+
+
+
Raises:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ MultipleResultsFound
+
+
+
+
If more than one record found
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Get paginated results using offset-based pagination.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ session
+
+
+ AsyncSession
+
+
+
+
+
+ required
+
+
+
+
+ filters
+
+
+ list [Any ] | None
+
+
+
+
List of SQLAlchemy filter conditions
+
+
+
+ None
+
+
+
+
+ joins
+
+
+ JoinType | None
+
+
+
+
List of (model, condition) tuples for joining related tables
+
+
+
+ None
+
+
+
+
+ outer_join
+
+
+ bool
+
+
+
+
Use LEFT OUTER JOIN instead of INNER JOIN
+
+
+
+ False
+
+
+
+
+ load_options
+
+
+ Sequence [ExecutableOption ] | None
+
+
+
+
SQLAlchemy loader options
+
+
+
+ None
+
+
+
+
+ order_by
+
+
+ OrderByClause | None
+
+
+
+
Column or list of columns to order by
+
+
+
+ None
+
+
+
+
+ page
+
+
+ int
+
+
+
+
Page number (1-indexed)
+
+
+
+ 1
+
+
+
+
+ items_per_page
+
+
+ int
+
+
+
+
Number of items per page
+
+
+
+ 20
+
+
+
+
+ search
+
+
+ str | SearchConfig | None
+
+
+
+
Search query string or SearchConfig object
+
+
+
+ None
+
+
+
+
+ search_fields
+
+
+ Sequence [SearchFieldType ] | None
+
+
+
+
Fields to search in (overrides class default)
+
+
+
+ None
+
+
+
+
+ facet_fields
+
+
+ Sequence [FacetFieldType ] | None
+
+
+
+
Columns to compute distinct values for (overrides class default)
+
+
+
+ None
+
+
+
+
+ filter_by
+
+
+ dict [str , Any ] | BaseModel | None
+
+
+
+
Dict of {column_key: value} to filter by declared facet fields.
+Keys must match the column.key of a facet field. Scalar → equality,
+list → IN clause. Raises InvalidFacetFilterError for unknown keys.
+
+
+
+ None
+
+
+
+
+ schema
+
+
+ type [BaseModel ]
+
+
+
+
Pydantic schema to serialize each item into.
+
+
+
+ required
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ PaginatedResponse [Any ]
+
+
+
+
PaginatedResponse with OffsetPagination metadata
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Return a FastAPI dependency that resolves order query params into an order_by clause.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ order_fields
+
+
+ Sequence [QueryableAttribute [Any ]] | None
+
+
+
+
Override the allowed order fields. Falls back to the class-level
+order_fields if not provided.
+
+
+
+ None
+
+
+
+
+ default_field
+
+
+ QueryableAttribute [Any ] | None
+
+
+
+
Field to order by when order_by query param is absent.
+If None and no order_by is provided, no ordering is applied.
+
+
+
+ None
+
+
+
+
+ default_order
+
+
+ Literal ['asc', 'desc']
+
+
+
+
Default order direction when order is absent
+("asc" or "desc").
+
+
+
+ 'asc'
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ Callable [..., Awaitable [OrderByClause | None]]
+
+
+
+
An async dependency function named {Model}OrderParams that resolves to an
+
+
+
+
+
+ Callable [..., Awaitable [OrderByClause | None]]
+
+
+
+
OrderByClause (or None). Pass it to Depends() in your route.
+
+
+
+
+
+
+
+
Raises:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ ValueError
+
+
+
+
If no order fields are configured on this CRUD class and none are
+provided via order_fields.
+
+
+
+
+
+ InvalidOrderFieldError
+
+
+
+
When the request provides an unknown order_by value.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
update (
+ session : AsyncSession ,
+ obj : BaseModel ,
+ filters : list [ Any ],
+ * ,
+ exclude_unset : bool = True ,
+ exclude_none : bool = False ,
+ schema : type [ SchemaType ],
+) -> Response [ SchemaType ]
+update (
+ session : AsyncSession ,
+ obj : BaseModel ,
+ filters : list [ Any ],
+ * ,
+ exclude_unset : bool = True ,
+ exclude_none : bool = False ,
+ schema : None = ... ,
+) -> ModelType
+
+
+
+
+
+
Update a record in the database.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ session
+
+
+ AsyncSession
+
+
+
+
+
+ required
+
+
+
+
+ obj
+
+
+ BaseModel
+
+
+
+
Pydantic model with update data
+
+
+
+ required
+
+
+
+
+ filters
+
+
+ list [Any ]
+
+
+
+
List of SQLAlchemy filter conditions
+
+
+
+ required
+
+
+
+
+ exclude_unset
+
+
+ bool
+
+
+
+
Exclude fields not explicitly set in the schema
+
+
+
+ True
+
+
+
+
+ exclude_none
+
+
+ bool
+
+
+
+
Exclude fields with None value
+
+
+
+ False
+
+
+
+
+ schema
+
+
+ type [BaseModel ] | None
+
+
+
+
Pydantic schema to serialize the result into. When provided,
+the result is automatically wrapped in a Response[schema].
+
+
+
+ None
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ ModelType | Response [Any ]
+
+
+
+
Updated model instance, or Response[schema] when schema is given.
+
+
+
+
+
+
+
+
Raises:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Create or update a record (PostgreSQL only).
+
Uses INSERT ... ON CONFLICT for atomic upsert.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ session
+
+
+ AsyncSession
+
+
+
+
+
+ required
+
+
+
+
+ obj
+
+
+ BaseModel
+
+
+
+
Pydantic model with data
+
+
+
+ required
+
+
+
+
+ index_elements
+
+
+ list [str ]
+
+
+
+
Columns for ON CONFLICT (unique constraint)
+
+
+
+ required
+
+
+
+
+ set_
+
+
+ BaseModel | None
+
+
+
+
Pydantic model for ON CONFLICT DO UPDATE SET
+
+
+
+ None
+
+
+
+
+ where
+
+
+ WhereHavingRole | None
+
+
+
+
WHERE clause for ON CONFLICT DO UPDATE
+
+
+
+ None
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ ModelType | None
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Create a CRUD class for a specific model.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ model
+
+
+ type [ModelType ]
+
+
+
+
SQLAlchemy model class
+
+
+
+ required
+
+
+
+
+ searchable_fields
+
+
+ Sequence [SearchFieldType ] | None
+
+
+
+
Optional list of searchable fields
+
+
+
+ None
+
+
+
+
+ facet_fields
+
+
+ Sequence [FacetFieldType ] | None
+
+
+
+
Optional list of columns to compute distinct values for in paginated
+responses. Supports direct columns (User.status) and relationship tuples
+((User.role, Role.name)). Can be overridden per call.
+
+
+
+ None
+
+
+
+
+ order_fields
+
+
+ Sequence [QueryableAttribute [Any ]] | None
+
+
+
+
Optional list of model attributes that callers are allowed to order by
+via order_params(). Can be overridden per call.
+
+
+
+ None
+
+
+
+
+ m2m_fields
+
+
+ M2MFieldType | None
+
+
+
+
Optional mapping for many-to-many relationships.
+Maps schema field names (containing lists of IDs) to
+SQLAlchemy relationship attributes.
+
+
+
+ None
+
+
+
+
+ default_load_options
+
+
+ Sequence [ExecutableOption ] | None
+
+
+
+
Default SQLAlchemy loader options applied to all read
+queries when no explicit load_options are passed. Use this
+instead of lazy="selectin" on the model so that loading
+strategy is explicit and per-CRUD. Overridden entirely (not
+merged) when load_options is provided at call-site.
+
+
+
+ None
+
+
+
+
+ cursor_column
+
+
+ Any | None
+
+
+
+
Required to call cursor_paginate.
+Must be monotonically ordered (e.g. integer PK, UUID v7, timestamp).
+See the cursor pagination docs for supported column types.
+
+
+
+ None
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ type [AsyncCrud [ModelType ]]
+
+
+
+
AsyncCrud subclass bound to the model
+
+
+
+
+
+
+
+
+ Example
+ from fastapi_toolsets.crud import CrudFactory
+from myapp.models import User , Post
+
+UserCrud = CrudFactory ( User )
+PostCrud = CrudFactory ( Post )
+
+# With searchable fields:
+UserCrud = CrudFactory (
+ User ,
+ searchable_fields = [ User . username , User . email , ( User . role , Role . name )]
+)
+
+# With many-to-many fields:
+# Schema has `tag_ids: list[UUID]`, model has `tags` relationship to Tag
+PostCrud = CrudFactory (
+ Post ,
+ m2m_fields = { "tag_ids" : Post . tags },
+)
+
+# With facet fields for filter dropdowns / faceted search:
+UserCrud = CrudFactory (
+ User ,
+ facet_fields = [ User . status , User . country , ( User . role , Role . name )],
+)
+
+# With a fixed cursor column for cursor_paginate:
+PostCrud = CrudFactory (
+ Post ,
+ cursor_column = Post . created_at ,
+)
+
+# With default load strategy (replaces lazy="selectin" on the model):
+ArticleCrud = CrudFactory (
+ Article ,
+ default_load_options = [ selectinload ( Article . category ), selectinload ( Article . tags )],
+)
+
+# Override default_load_options for a specific call:
+article = await ArticleCrud . get (
+ session ,
+ [ Article . id == 1 ],
+ load_options = [ selectinload ( Article . category )], # tags won't load
+)
+
+# Usage
+user = await UserCrud . get ( session , [ User . id == 1 ])
+posts = await PostCrud . get_multi ( session , filters = [ Post . user_id == user . id ])
+
+# Create with M2M - tag_ids are automatically resolved
+post = await PostCrud . create ( session , PostCreate ( title = "Hello" , tag_ids = [ id1 , id2 ]))
+
+# With search
+result = await UserCrud . offset_paginate ( session , search = "john" )
+
+# With joins (inner join by default):
+users = await UserCrud . get_multi (
+ session ,
+ joins = [( Post , Post . user_id == User . id )],
+ filters = [ Post . published == True ],
+)
+
+# With outer join:
+users = await UserCrud . get_multi (
+ session ,
+ joins = [( Post , Post . user_id == User . id )],
+ outer_join = True ,
+)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Advanced search configuration.
+
+
+
Attributes:
+
+
+
+ Name
+ Type
+ Description
+
+
+
+
+ query
+
+ str
+
+
+
+
+
+
+ fields
+
+ Sequence [SearchFieldType ] | None
+
+
+
+
Fields to search (columns or tuples for relationships)
+
+
+
+
+ case_sensitive
+
+ bool
+
+
+
+
Case-sensitive search (default: False)
+
+
+
+
+ match_mode
+
+ Literal ['any', 'all']
+
+
+
+
"any" (OR) or "all" (AND) to combine fields
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Auto-detect String fields on a model and its relationships.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ model
+
+
+ type [DeclarativeBase ]
+
+
+
+
SQLAlchemy model class
+
+
+
+ required
+
+
+
+
+ include_relationships
+
+
+ bool
+
+
+
+
Include fields from many-to-one/one-to-one relationships
+
+
+
+ True
+
+
+
+
+ max_depth
+
+
+ int
+
+
+
+
Max depth for relationship traversal (default: 1)
+
+
+
+ 1
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ list [SearchFieldType ]
+
+
+
+
List of columns and tuples (relationship, column)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Build SQLAlchemy filter conditions for search.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ model
+
+
+ type [DeclarativeBase ]
+
+
+
+
SQLAlchemy model class
+
+
+
+ required
+
+
+
+
+ search
+
+
+ str | SearchConfig
+
+
+
+
Search string or SearchConfig
+
+
+
+ required
+
+
+
+
+ search_fields
+
+
+ Sequence [SearchFieldType ] | None
+
+
+
+
Fields specified per-call (takes priority)
+
+
+
+ None
+
+
+
+
+ default_fields
+
+
+ Sequence [SearchFieldType ] | None
+
+
+
+
Default fields (from ClassVar)
+
+
+
+ None
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ tuple [list [ColumnElement [bool ]], list [InstrumentedAttribute [Any ]]]
+
+
+
+
Tuple of (filter_conditions, joins_needed)
+
+
+
+
+
+
+
+
Raises:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ NoSearchableFieldsError
+
+
+
+
If no searchable field has been configured
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back to top
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v2.2/reference/db/index.html b/v2.2/reference/db/index.html
new file mode 100644
index 0000000..d3c7bf9
--- /dev/null
+++ b/v2.2/reference/db/index.html
@@ -0,0 +1,2577 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ db - FastAPI Toolsets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Home
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Reference
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+db
+Here's the reference for all database session utilities, transaction helpers, and locking functions.
+You can import them directly from fastapi_toolsets.db:
+from fastapi_toolsets.db import (
+ LockMode ,
+ cleanup_tables ,
+ create_database ,
+ create_db_dependency ,
+ create_db_context ,
+ get_transaction ,
+ lock_tables ,
+ wait_for_row_change ,
+)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Create a FastAPI dependency for database sessions.
+
Creates a dependency function that yields a session and auto-commits
+if a transaction is active when the request completes.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ session_maker
+
+
+ async_sessionmaker [AsyncSession ]
+
+
+
+
Async session factory from create_session_factory()
+
+
+
+ required
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ Callable [[], AsyncGenerator [AsyncSession , None]]
+
+
+
+
An async generator function usable with FastAPI's Depends()
+
+
+
+
+
+
+
+
+ Example
+ from fastapi import Depends
+from sqlalchemy.ext.asyncio import AsyncSession , create_async_engine , async_sessionmaker
+from fastapi_toolsets.db import create_db_dependency
+
+engine = create_async_engine ( "postgresql+asyncpg://..." )
+SessionLocal = async_sessionmaker ( engine , expire_on_commit = False )
+get_db = create_db_dependency ( SessionLocal )
+
+@app . get ( "/users" )
+async def list_users ( session : AsyncSession = Depends ( get_db )):
+ ...
+
+
+
+
+
+
+
+
+
+
+
+ fastapi_toolsets . db . create_db_context ( session_maker )
+
+
+
+
+
+
+
Create a context manager for database sessions.
+
Creates a context manager for use outside of FastAPI request handlers,
+such as in background tasks, CLI commands, or tests.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ session_maker
+
+
+ async_sessionmaker [AsyncSession ]
+
+
+
+
Async session factory from create_session_factory()
+
+
+
+ required
+
+
+
+
+
+
+
Returns:
+
+
+
+
+ Example
+ from sqlalchemy.ext.asyncio import create_async_engine , async_sessionmaker
+from fastapi_toolsets.db import create_db_context
+
+engine = create_async_engine ( "postgresql+asyncpg://..." )
+SessionLocal = async_sessionmaker ( engine , expire_on_commit = False )
+get_db_context = create_db_context ( SessionLocal )
+
+async def background_task ():
+ async with get_db_context () as session :
+ user = await UserCrud . get ( session , [ User . id == 1 ])
+ ...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Get a transaction context, handling nested transactions.
+
If already in a transaction, creates a savepoint (nested transaction).
+Otherwise, starts a new transaction.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ session
+
+
+ AsyncSession
+
+
+
+
AsyncSession instance
+
+
+
+ required
+
+
+
+
+
+
+
Yields:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ AsyncGenerator [AsyncSession , None]
+
+
+
+
The session within the transaction context
+
+
+
+
+
+
+
+
+ Example
+ async with get_transaction ( session ):
+ session . add ( model )
+ # Auto-commits on exit, rolls back on exception
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Lock PostgreSQL tables for the duration of a transaction.
+
Acquires table-level locks that are held until the transaction ends.
+Useful for preventing concurrent modifications during critical operations.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ session
+
+
+ AsyncSession
+
+
+
+
AsyncSession instance
+
+
+
+ required
+
+
+
+
+ tables
+
+
+ list [type [DeclarativeBase ]]
+
+
+
+
List of SQLAlchemy model classes to lock
+
+
+
+ required
+
+
+
+
+ mode
+
+
+ LockMode
+
+
+
+
Lock mode (default: SHARE UPDATE EXCLUSIVE)
+
+
+
+ SHARE_UPDATE_EXCLUSIVE
+
+
+
+
+ timeout
+
+
+ str
+
+
+
+
Lock timeout (default: "5s")
+
+
+
+ '5s'
+
+
+
+
+
+
+
Yields:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ AsyncGenerator [AsyncSession , None]
+
+
+
+
The session with locked tables
+
+
+
+
+
+
+
+
Raises:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ SQLAlchemyError
+
+
+
+
If lock cannot be acquired within timeout
+
+
+
+
+
+
+
+
+ Example
+ from fastapi_toolsets.db import lock_tables , LockMode
+
+async with lock_tables ( session , [ User , Account ]):
+ # Tables are locked with SHARE UPDATE EXCLUSIVE mode
+ user = await UserCrud . get ( session , [ User . id == 1 ])
+ user . balance += 100
+
+# With custom lock mode
+async with lock_tables ( session , [ Order ], mode = LockMode . EXCLUSIVE ):
+ # Exclusive lock - no other transactions can access
+ await process_order ( session , order_id )
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Poll a database row until a change is detected.
+
Queries the row every interval seconds and returns the model instance
+once a change is detected in any column (or only the specified columns).
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ session
+
+
+ AsyncSession
+
+
+
+
AsyncSession instance
+
+
+
+ required
+
+
+
+
+ model
+
+
+ type [_M ]
+
+
+
+
SQLAlchemy model class
+
+
+
+ required
+
+
+
+
+ pk_value
+
+
+ Any
+
+
+
+
Primary key value of the row to watch
+
+
+
+ required
+
+
+
+
+ columns
+
+
+ list [str ] | None
+
+
+
+
Optional list of column names to watch. If None, all columns
+ are watched.
+
+
+
+ None
+
+
+
+
+ interval
+
+
+ float
+
+
+
+
Polling interval in seconds (default: 0.5)
+
+
+
+ 0.5
+
+
+
+
+ timeout
+
+
+ float | None
+
+
+
+
Maximum time to wait in seconds. None means wait forever.
+
+
+
+ None
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ _M
+
+
+
+
The refreshed model instance with updated values
+
+
+
+
+
+
+
+
Raises:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ NotFoundError
+
+
+
+
If the row does not exist or is deleted during polling
+
+
+
+
+
+ TimeoutError
+
+
+
+
If timeout expires before a change is detected
+
+
+
+
+
+
+
+
+ Example
+ from fastapi_toolsets.db import wait_for_row_change
+
+# Wait for any column to change
+updated = await wait_for_row_change ( session , User , user_id )
+
+# Watch specific columns with a timeout
+updated = await wait_for_row_change (
+ session , User , user_id ,
+ columns = [ "status" , "email" ],
+ interval = 1.0 ,
+ timeout = 30.0 ,
+)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Create a database.
+
Connects to server_url using AUTOCOMMIT isolation and issues a
+CREATE DATABASE statement for db_name .
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ db_name
+
+
+ str
+
+
+
+
Name of the database to create.
+
+
+
+ required
+
+
+
+
+ server_url
+
+
+ str
+
+
+
+
URL used for server-level DDL (must point to an existing
+database on the same server).
+
+
+
+ required
+
+
+
+
+
+
+
+ Example
+ from fastapi_toolsets.db import create_database
+
+SERVER_URL = "postgresql+asyncpg://postgres:postgres@localhost/postgres"
+await create_database ( "myapp_test" , server_url = SERVER_URL )
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Truncate all tables for fast between-test cleanup.
+
Executes a single TRUNCATE … RESTART IDENTITY CASCADE statement
+across every table in base 's metadata, which is significantly faster
+than dropping and re-creating tables between tests.
+
This is a no-op when the metadata contains no tables.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ session
+
+
+ AsyncSession
+
+
+
+
An active async database session.
+
+
+
+ required
+
+
+
+
+ base
+
+
+ type [DeclarativeBase ]
+
+
+
+
SQLAlchemy DeclarativeBase class containing model metadata.
+
+
+
+ required
+
+
+
+
+
+
+
+ Example
+ @pytest . fixture
+async def db_session ( worker_db_url ):
+ async with create_db_session ( worker_db_url , Base ) as session :
+ yield session
+ await cleanup_tables ( session , Base )
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back to top
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v2.2/reference/dependencies/index.html b/v2.2/reference/dependencies/index.html
new file mode 100644
index 0000000..b5ff276
--- /dev/null
+++ b/v2.2/reference/dependencies/index.html
@@ -0,0 +1,1959 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ dependencies - FastAPI Toolsets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Home
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Reference
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+dependencies
+Here's the reference for the FastAPI dependency factory functions.
+You can import them directly from fastapi_toolsets.dependencies:
+from fastapi_toolsets.dependencies import PathDependency , BodyDependency
+
+
+
+
+
+
+
+
+
+
+
+
Create a dependency that fetches a DB object from a path parameter.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ model
+
+
+ type [ModelType ]
+
+
+
+
SQLAlchemy model class
+
+
+
+ required
+
+
+
+
+ field
+
+
+ Any
+
+
+
+
Model field to filter by (e.g., User.id)
+
+
+
+ required
+
+
+
+
+ session_dep
+
+
+ SessionDependency
+
+
+
+
Session dependency function (e.g., get_db)
+
+
+
+ required
+
+
+
+
+ param_name
+
+
+ str | None
+
+
+
+
Path parameter name (defaults to model_field, e.g., user_id)
+
+
+
+ None
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ ModelType
+
+
+
+
A Depends() instance that resolves to the model instance
+
+
+
+
+
+
+
+
Raises:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ NotFoundError
+
+
+
+
If no matching record is found
+
+
+
+
+
+
+
+
+ Example
+ UserDep = PathDependency ( User , User . id , session_dep = get_db )
+
+@router . get ( "/user/ {id} " )
+async def get (
+ user : User = UserDep ,
+): ...
+
+
+
+
+
+
+
+
+
+
+
+ fastapi_toolsets . dependencies . BodyDependency ( model , field , * , session_dep , body_field )
+
+
+
+
+
+
+
Create a dependency that fetches a DB object from a body field.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ model
+
+
+ type [ModelType ]
+
+
+
+
SQLAlchemy model class
+
+
+
+ required
+
+
+
+
+ field
+
+
+ Any
+
+
+
+
Model field to filter by (e.g., User.id)
+
+
+
+ required
+
+
+
+
+ session_dep
+
+
+ SessionDependency
+
+
+
+
Session dependency function (e.g., get_db)
+
+
+
+ required
+
+
+
+
+ body_field
+
+
+ str
+
+
+
+
Name of the field in the request body
+
+
+
+ required
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ ModelType
+
+
+
+
A Depends() instance that resolves to the model instance
+
+
+
+
+
+
+
+
Raises:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ NotFoundError
+
+
+
+
If no matching record is found
+
+
+
+
+
+
+
+
+ Example
+ UserDep = BodyDependency (
+ User , User . ctfd_id , session_dep = get_db , body_field = "user_id"
+)
+
+@router . post ( "/assign" )
+async def assign (
+ user : User = UserDep ,
+): ...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back to top
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v2.2/reference/exceptions/index.html b/v2.2/reference/exceptions/index.html
new file mode 100644
index 0000000..183aa98
--- /dev/null
+++ b/v2.2/reference/exceptions/index.html
@@ -0,0 +1,3006 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ exceptions - FastAPI Toolsets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Home
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Reference
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+exceptions
+Here's the reference for all exception classes and handler utilities.
+You can import them directly from fastapi_toolsets.exceptions:
+from fastapi_toolsets.exceptions import (
+ ApiException ,
+ UnauthorizedError ,
+ ForbiddenError ,
+ NotFoundError ,
+ ConflictError ,
+ NoSearchableFieldsError ,
+ InvalidFacetFilterError ,
+ InvalidOrderFieldError ,
+ generate_error_responses ,
+ init_exceptions_handlers ,
+)
+
+
+
+
+
+
+
+
+
+
+
+
+ Bases: Exception
+
+
+
+
Base exception for API errors with structured response.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Initialize the exception.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ detail
+
+
+ str | None
+
+
+
+
Optional human-readable message
+
+
+
+ None
+
+
+
+
+ desc
+
+
+ str | None
+
+
+
+
Optional per-instance override for the description field
+in the HTTP response body.
+
+
+
+ None
+
+
+
+
+ data
+
+
+ Any
+
+
+
+
Optional per-instance override for the data field in the
+HTTP response body.
+
+
+
+ None
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bases: ApiException
+
+
+
+
HTTP 401 - User is not authenticated.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Initialize the exception.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ detail
+
+
+ str | None
+
+
+
+
Optional human-readable message
+
+
+
+ None
+
+
+
+
+ desc
+
+
+ str | None
+
+
+
+
Optional per-instance override for the description field
+in the HTTP response body.
+
+
+
+ None
+
+
+
+
+ data
+
+
+ Any
+
+
+
+
Optional per-instance override for the data field in the
+HTTP response body.
+
+
+
+ None
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bases: ApiException
+
+
+
+
HTTP 403 - User lacks required permissions.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Initialize the exception.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ detail
+
+
+ str | None
+
+
+
+
Optional human-readable message
+
+
+
+ None
+
+
+
+
+ desc
+
+
+ str | None
+
+
+
+
Optional per-instance override for the description field
+in the HTTP response body.
+
+
+
+ None
+
+
+
+
+ data
+
+
+ Any
+
+
+
+
Optional per-instance override for the data field in the
+HTTP response body.
+
+
+
+ None
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bases: ApiException
+
+
+
+
HTTP 404 - Resource not found.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Initialize the exception.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ detail
+
+
+ str | None
+
+
+
+
Optional human-readable message
+
+
+
+ None
+
+
+
+
+ desc
+
+
+ str | None
+
+
+
+
Optional per-instance override for the description field
+in the HTTP response body.
+
+
+
+ None
+
+
+
+
+ data
+
+
+ Any
+
+
+
+
Optional per-instance override for the data field in the
+HTTP response body.
+
+
+
+ None
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bases: ApiException
+
+
+
+
HTTP 409 - Resource conflict.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Initialize the exception.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ detail
+
+
+ str | None
+
+
+
+
Optional human-readable message
+
+
+
+ None
+
+
+
+
+ desc
+
+
+ str | None
+
+
+
+
Optional per-instance override for the description field
+in the HTTP response body.
+
+
+
+ None
+
+
+
+
+ data
+
+
+ Any
+
+
+
+
Optional per-instance override for the data field in the
+HTTP response body.
+
+
+
+ None
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bases: ApiException
+
+
+
+
Raised when search is requested but no searchable fields are available.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Initialize the exception.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ model
+
+
+ type
+
+
+
+
The model class that has no searchable fields configured.
+
+
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bases: ApiException
+
+
+
+
Raised when filter_by contains a key not declared in facet_fields.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Initialize the exception.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ key
+
+
+ str
+
+
+
+
The unknown filter key provided by the caller.
+
+
+
+ required
+
+
+
+
+ valid_keys
+
+
+ set [str ]
+
+
+
+
Set of valid keys derived from the declared facet_fields.
+
+
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bases: ApiException
+
+
+
+
Raised when order_by contains a field not in the allowed order fields.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Initialize the exception.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ field
+
+
+ str
+
+
+
+
The unknown order field provided by the caller.
+
+
+
+ required
+
+
+
+
+ valid_fields
+
+
+ list [str ]
+
+
+
+
List of valid field names.
+
+
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Generate OpenAPI response documentation for exceptions.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ *errors
+
+
+ type [ApiException ]
+
+
+
+
Exception classes that inherit from ApiException.
+
+
+
+ ()
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ dict [int | str , dict [str , Any ]]
+
+
+
+
Dict suitable for FastAPI's responses parameter.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Register exception handlers and custom OpenAPI schema on a FastAPI app.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ app
+
+
+ FastAPI
+
+
+
+
FastAPI application instance.
+
+
+
+ required
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ FastAPI
+
+
+
+
The same FastAPI instance (for chaining).
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back to top
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v2.2/reference/fixtures/index.html b/v2.2/reference/fixtures/index.html
new file mode 100644
index 0000000..26f2f63
--- /dev/null
+++ b/v2.2/reference/fixtures/index.html
@@ -0,0 +1,3076 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ fixtures - FastAPI Toolsets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Home
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Reference
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+fixtures
+Here's the reference for the fixture registry, enums, and loading utilities.
+You can import them directly from fastapi_toolsets.fixtures:
+from fastapi_toolsets.fixtures import (
+ Context ,
+ LoadStrategy ,
+ Fixture ,
+ FixtureRegistry ,
+ load_fixtures ,
+ load_fixtures_by_context ,
+ get_obj_by_attr ,
+)
+
+
+
+
+
+
+
+
+ fastapi_toolsets.fixtures.enum.Context
+
+
+
+
+
+
+
+ Bases: str , Enum
+
+
+
+
Predefined fixture contexts.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ BASE = 'base'
+
+
+ class-attribute
+ instance-attribute
+
+
+
+
+
+
+
+
Base fixtures loaded in all environments.
+
+
+
+
+
+
+
+
+
+
+ DEVELOPMENT = 'development'
+
+
+ class-attribute
+ instance-attribute
+
+
+
+
+
+
+
+
Development fixtures.
+
+
+
+
+
+
+
+
+
+
+ PRODUCTION = 'production'
+
+
+ class-attribute
+ instance-attribute
+
+
+
+
+
+
+
+
Production-only fixtures.
+
+
+
+
+
+
+
+
+
+
+ TESTING = 'testing'
+
+
+ class-attribute
+ instance-attribute
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bases: str , Enum
+
+
+
+
Strategy for loading fixtures into the database.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Insert new records. Fails if record already exists.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Insert or update based on primary key (SQLAlchemy merge).
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Insert only if record doesn't exist (based on primary key).
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
A fixture definition with metadata.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Registry for managing fixtures with dependencies.
+
+
+
+ Example
+ from fastapi_toolsets.fixtures import FixtureRegistry , Context
+
+fixtures = FixtureRegistry ()
+
+@fixtures . register
+def roles ():
+ return [
+ Role ( id = 1 , name = "admin" ),
+ Role ( id = 2 , name = "user" ),
+ ]
+
+@fixtures . register ( depends_on = [ "roles" ])
+def users ():
+ return [
+ User ( id = 1 , username = "admin" , role_id = 1 ),
+ ]
+
+@fixtures . register ( depends_on = [ "users" ], contexts = [ Context . TESTING ])
+def test_data ():
+ return [
+ Post ( id = 1 , title = "Test" , user_id = 1 ),
+ ]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Get a fixture by name.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Get all registered fixtures.
+
+
+
+
+
+
+
+
+
+
+ get_by_context ( * contexts )
+
+
+
+
+
+
+
Get fixtures for specific contexts.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Include another FixtureRegistry in the same current FixtureRegistry.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ registry
+
+
+ FixtureRegistry
+
+
+
+
The FixtureRegistry to include
+
+
+
+ required
+
+
+
+
+
+
+
Raises:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ ValueError
+
+
+
+
If a fixture name already exists in the current registry
+
+
+
+
+
+
+
+
+ Example
+ registry = FixtureRegistry ()
+dev_registry = FixtureRegistry ()
+
+@dev_registry . register
+def dev_data ():
+ return [ ... ]
+
+registry . include_registry ( registry = dev_registry )
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Register a fixture function.
+
Can be used as a decorator with or without arguments.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ func
+
+
+ Callable [[], Sequence [DeclarativeBase ]] | None
+
+
+
+
Fixture function returning list of model instances
+
+
+
+ None
+
+
+
+
+ name
+
+
+ str | None
+
+
+
+
Fixture name (defaults to function name)
+
+
+
+ None
+
+
+
+
+ depends_on
+
+
+ list [str ] | None
+
+
+
+
List of fixture names this depends on
+
+
+
+ None
+
+
+
+
+ contexts
+
+
+ list [str | Context ] | None
+
+
+
+
List of contexts this fixture belongs to
+
+
+
+ None
+
+
+
+
+
+
+
+ Example
+ @fixtures . register
+def roles ():
+ return [ Role ( id = 1 , name = "admin" )]
+
+@fixtures . register ( depends_on = [ "roles" ], contexts = [ Context . TESTING ])
+def test_users ():
+ return [ User ( id = 1 , username = "test" , role_id = 1 )]
+
+
+
+
+
+
+
+
+
+
+
+ resolve_context_dependencies ( * contexts )
+
+
+
+
+
+
+
Resolve all fixtures for contexts with dependencies.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ *contexts
+
+
+ str | Context
+
+
+
+
+
+ ()
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ list [str ]
+
+
+
+
List of fixture names in load order
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Resolve fixture dependencies in topological order.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ *names
+
+
+ str
+
+
+
+
Fixture names to resolve
+
+
+
+ ()
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ list [str ]
+
+
+
+
List of fixture names in load order (dependencies first)
+
+
+
+
+
+
+
+
Raises:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ KeyError
+
+
+
+
If a fixture is not found
+
+
+
+
+
+ ValueError
+
+
+
+
If circular dependency detected
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Load specific fixtures by name with dependencies.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ session
+
+
+ AsyncSession
+
+
+
+
+
+ required
+
+
+
+
+ registry
+
+
+ FixtureRegistry
+
+
+
+
+
+ required
+
+
+
+
+ *names
+
+
+ str
+
+
+
+
Fixture names to load (dependencies auto-resolved)
+
+
+
+ ()
+
+
+
+
+ strategy
+
+
+ LoadStrategy
+
+
+
+
How to handle existing records
+
+
+
+ MERGE
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ dict [str , list [DeclarativeBase ]]
+
+
+
+
Dict mapping fixture names to loaded instances
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ fastapi_toolsets . fixtures . utils . load_fixtures_by_context ( session , registry , * contexts , strategy = LoadStrategy . MERGE )
+
+
+ async
+
+
+
+
+
+
+
+
Load all fixtures for specific contexts.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ session
+
+
+ AsyncSession
+
+
+
+
+
+ required
+
+
+
+
+ registry
+
+
+ FixtureRegistry
+
+
+
+
+
+ required
+
+
+
+
+ *contexts
+
+
+ str | Context
+
+
+
+
Contexts to load (e.g., Context.BASE, Context.TESTING)
+
+
+
+ ()
+
+
+
+
+ strategy
+
+
+ LoadStrategy
+
+
+
+
How to handle existing records
+
+
+
+ MERGE
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ dict [str , list [DeclarativeBase ]]
+
+
+
+
Dict mapping fixture names to loaded instances
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Get a SQLAlchemy model instance by matching an attribute value.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ fixtures
+
+
+ Callable [[], Sequence [ModelType ]]
+
+
+
+
A fixture function registered via @registry.register
+that returns a sequence of SQLAlchemy model instances.
+
+
+
+ required
+
+
+
+
+ attr_name
+
+
+ str
+
+
+
+
Name of the attribute to match against.
+
+
+
+ required
+
+
+
+
+ value
+
+
+ Any
+
+
+
+
+
+ required
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ ModelType
+
+
+
+
The first model instance where the attribute matches the given value.
+
+
+
+
+
+
+
+
Raises:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ StopIteration
+
+
+
+
If no matching object is found in the fixture group.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back to top
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v2.2/reference/logger/index.html b/v2.2/reference/logger/index.html
new file mode 100644
index 0000000..bea3c33
--- /dev/null
+++ b/v2.2/reference/logger/index.html
@@ -0,0 +1,1858 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ logger - FastAPI Toolsets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Home
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Reference
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+logger
+Here's the reference for the logging utilities.
+You can import them directly from fastapi_toolsets.logger:
+from fastapi_toolsets.logger import configure_logging , get_logger
+
+
+
+
+
+
+
+
+
+
+
+
Configure logging with a stdout handler and consistent format.
+
Sets up a :class:~logging.StreamHandler writing to stdout with the
+given format and level. Also configures the uvicorn loggers so that
+FastAPI access logs use the same format.
+
Calling this function multiple times is safe -- existing handlers are
+replaced rather than duplicated.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ level
+
+
+ LogLevel | int
+
+
+
+
Log level (e.g. "DEBUG", "INFO", or logging.DEBUG).
+
+
+
+ 'INFO'
+
+
+
+
+ fmt
+
+
+ str
+
+
+
+
Log format string. Defaults to
+"%(asctime)s - %(name)s - %(levelname)s - %(message)s".
+
+
+
+ DEFAULT_FORMAT
+
+
+
+
+ logger_name
+
+
+ str | None
+
+
+
+
Logger name to configure. None (the default)
+configures the root logger so all loggers inherit the settings.
+
+
+
+ None
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ Logger
+
+
+
+
The configured Logger instance.
+
+
+
+
+
+
+
+
+ Example
+ from fastapi_toolsets.logger import configure_logging
+
+logger = configure_logging ( "DEBUG" )
+logger . info ( "Application started" )
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Return a logger with the given name .
+
A thin convenience wrapper around :func:logging.getLogger that keeps
+logging imports consistent across the codebase.
+
When called without arguments, the caller's __name__ is used
+automatically, so get_logger() in a module is equivalent to
+logging.getLogger(__name__). Pass None explicitly to get the
+root logger.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ name
+
+
+ str | None
+
+
+
+
Logger name. Defaults to the caller's __name__.
+Pass None to get the root logger.
+
+
+
+ _SENTINEL
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ Logger
+
+
+
+
+
+
+
+
+
+
+ Example
+ from fastapi_toolsets.logger import get_logger
+
+logger = get_logger () # uses caller's __name__
+logger = get_logger ( "myapp" ) # explicit name
+logger = get_logger ( None ) # root logger
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back to top
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v2.2/reference/metrics/index.html b/v2.2/reference/metrics/index.html
new file mode 100644
index 0000000..be410ef
--- /dev/null
+++ b/v2.2/reference/metrics/index.html
@@ -0,0 +1,2225 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ metrics - FastAPI Toolsets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Home
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Reference
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+metrics
+Here's the reference for the Prometheus metrics registry and endpoint handler.
+You can import them directly from fastapi_toolsets.metrics:
+from fastapi_toolsets.metrics import Metric , MetricsRegistry , init_metrics
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
A metric definition with metadata.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Registry for managing Prometheus metric providers and collectors.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Return the metric instance created by a provider.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ name
+
+
+ str
+
+
+
+
The metric name (defaults to the provider function name).
+
+
+
+ required
+
+
+
+
+
+
+
Raises:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ KeyError
+
+
+
+
If the metric name is unknown or init_metrics has not
+been called yet.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Get all registered metric definitions.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Get collectors (called on each scrape).
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Get metric providers (called once at init).
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Include another :class:MetricsRegistry into this one.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ registry
+
+
+ MetricsRegistry
+
+
+
+
The registry to merge in.
+
+
+
+ required
+
+
+
+
+
+
+
Raises:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ ValueError
+
+
+
+
If a metric name already exists in the current registry.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Register a metric provider or collector function.
+
Can be used as a decorator with or without arguments.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ func
+
+
+ Callable [..., Any ] | None
+
+
+
+
The metric function to register.
+
+
+
+ None
+
+
+
+
+ name
+
+
+ str | None
+
+
+
+
Metric name (defaults to function name).
+
+
+
+ None
+
+
+
+
+ collect
+
+
+ bool
+
+
+
+
If True, the function is called on every scrape.
+If False (default), called once at init time.
+
+
+
+ False
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Register a Prometheus /metrics endpoint on a FastAPI app.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ app
+
+
+ FastAPI
+
+
+
+
FastAPI application instance.
+
+
+
+ required
+
+
+
+
+ registry
+
+
+ MetricsRegistry
+
+
+
+
A :class:MetricsRegistry containing providers and collectors.
+
+
+
+ required
+
+
+
+
+ path
+
+
+ str
+
+
+
+
URL path for the metrics endpoint (default /metrics).
+
+
+
+ '/metrics'
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ FastAPI
+
+
+
+
The same FastAPI instance (for chaining).
+
+
+
+
+
+
+
+
+ Example
+ from fastapi import FastAPI
+from fastapi_toolsets.metrics import MetricsRegistry , init_metrics
+
+metrics = MetricsRegistry ()
+app = FastAPI ()
+init_metrics ( app , registry = metrics )
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back to top
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v2.2/reference/models/index.html b/v2.2/reference/models/index.html
new file mode 100644
index 0000000..ea18782
--- /dev/null
+++ b/v2.2/reference/models/index.html
@@ -0,0 +1,1862 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ models - FastAPI Toolsets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Home
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Reference
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+models
+Here's the reference for the SQLAlchemy model mixins provided by the models module.
+You can import them directly from fastapi_toolsets.models:
+from fastapi_toolsets.models import (
+ UUIDMixin ,
+ CreatedAtMixin ,
+ UpdatedAtMixin ,
+ TimestampMixin ,
+)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Mixin that adds a UUID primary key auto-generated by the database.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Mixin that adds a created_at timestamp column.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Mixin that adds an updated_at timestamp column.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bases: CreatedAtMixin , UpdatedAtMixin
+
+
+
+
Mixin that combines created_at and updated_at timestamp columns.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back to top
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v2.2/reference/pytest/index.html b/v2.2/reference/pytest/index.html
new file mode 100644
index 0000000..fcdc345
--- /dev/null
+++ b/v2.2/reference/pytest/index.html
@@ -0,0 +1,2385 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ pytest - FastAPI Toolsets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Home
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Reference
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+pytest
+Here's the reference for all testing utilities and pytest fixtures.
+You can import them directly from fastapi_toolsets.pytest:
+from fastapi_toolsets.pytest import (
+ register_fixtures ,
+ create_async_client ,
+ create_db_session ,
+ worker_database_url ,
+ create_worker_database ,
+ cleanup_tables ,
+)
+
+
+
+
+
+
+
+
+
+
+
+
Register pytest fixtures from a FixtureRegistry.
+
Automatically creates pytest fixtures for each fixture in the registry.
+Dependencies are resolved via pytest fixture dependencies.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ registry
+
+
+ FixtureRegistry
+
+
+
+
The FixtureRegistry containing fixtures
+
+
+
+ required
+
+
+
+
+ namespace
+
+
+ dict [str , Any ]
+
+
+
+
The module's globals() dict to add fixtures to
+
+
+
+ required
+
+
+
+
+ prefix
+
+
+ str
+
+
+
+
Prefix for generated fixture names (default: "fixture_")
+
+
+
+ 'fixture_'
+
+
+
+
+ session_fixture
+
+
+ str
+
+
+
+
Name of the db session fixture (default: "db_session")
+
+
+
+ 'db_session'
+
+
+
+
+ strategy
+
+
+ LoadStrategy
+
+
+
+
Loading strategy for fixtures (default: MERGE)
+
+
+
+ MERGE
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ list [str ]
+
+
+
+
List of created fixture names
+
+
+
+
+
+
+
+
+ Example
+ # conftest.py
+from app.fixtures import fixtures
+from fastapi_toolsets.pytest_plugin import register_fixtures
+
+register_fixtures ( fixtures , globals ())
+
+# Creates fixtures like:
+# - fixture_roles
+# - fixture_users (depends on fixture_roles if users depends on roles)
+# - fixture_posts (depends on fixture_users if posts depends on users)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Create an async httpx client for testing FastAPI applications.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ app
+
+
+ Any
+
+
+
+
FastAPI application instance.
+
+
+
+ required
+
+
+
+
+ base_url
+
+
+ str
+
+
+
+
+
+ 'http://test'
+
+
+
+
+ dependency_overrides
+
+
+ dict [Callable [..., Any ], Callable [..., Any ]] | None
+
+
+
+
Optional mapping of original dependencies to
+their test replacements. Applied via app.dependency_overrides
+before yielding and cleaned up after.
+
+
+
+ None
+
+
+
+
+
+
+
Yields:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ AsyncGenerator [AsyncClient , None]
+
+
+
+
An AsyncClient configured for the app.
+
+
+
+
+
+
+
+
+ Example
+ from fastapi import FastAPI
+from fastapi_toolsets.pytest import create_async_client
+
+app = FastAPI ()
+
+@pytest . fixture
+async def client ():
+ async with create_async_client ( app ) as c :
+ yield c
+
+async def test_endpoint ( client : AsyncClient ):
+ response = await client . get ( "/health" )
+ assert response . status_code == 200
+
+
+
+
+ Example with dependency overrides
+ from fastapi_toolsets.pytest import create_async_client , create_db_session
+from app.db import get_db
+
+@pytest . fixture
+async def db_session ():
+ async with create_db_session ( DATABASE_URL , Base , cleanup = True ) as session :
+ yield session
+
+@pytest . fixture
+async def client ( db_session ):
+ async def override ():
+ yield db_session
+
+ async with create_async_client (
+ app , dependency_overrides = { get_db : override }
+ ) as c :
+ yield c
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Create a database session for testing.
+
Creates tables before yielding the session and optionally drops them after.
+Each call creates a fresh engine and session for test isolation.
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ database_url
+
+
+ str
+
+
+
+
Database connection URL (e.g., "postgresql+asyncpg://...").
+
+
+
+ required
+
+
+
+
+ base
+
+
+ type [DeclarativeBase ]
+
+
+
+
SQLAlchemy DeclarativeBase class containing model metadata.
+
+
+
+ required
+
+
+
+
+ echo
+
+
+ bool
+
+
+
+
Enable SQLAlchemy query logging. Defaults to False.
+
+
+
+ False
+
+
+
+
+ expire_on_commit
+
+
+ bool
+
+
+
+
Expire objects after commit. Defaults to False.
+
+
+
+ False
+
+
+
+
+ drop_tables
+
+
+ bool
+
+
+
+
Drop tables after test. Defaults to True.
+
+
+
+ True
+
+
+
+
+ cleanup
+
+
+ bool
+
+
+
+
Truncate all tables after test using
+:func:cleanup_tables. Defaults to False.
+
+
+
+ False
+
+
+
+
+
+
+
Yields:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ AsyncGenerator [AsyncSession , None]
+
+
+
+
An AsyncSession ready for database operations.
+
+
+
+
+
+
+
+
+ Example
+ from fastapi_toolsets.pytest import create_db_session
+from app.models import Base
+
+DATABASE_URL = "postgresql+asyncpg://user:pass@localhost/test_db"
+
+@pytest . fixture
+async def db_session ():
+ async with create_db_session (
+ DATABASE_URL , Base , cleanup = True
+ ) as session :
+ yield session
+
+async def test_create_user ( db_session : AsyncSession ):
+ user = User ( name = "test" )
+ db_session . add ( user )
+ await db_session . commit ()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Derive a per-worker database URL for pytest-xdist parallel runs.
+
Appends _{worker_name} to the database name so each xdist worker
+operates on its own database. When not running under xdist,
+_{default_test_db} is appended instead.
+
The worker name is read from the PYTEST_XDIST_WORKER environment
+variable (set automatically by xdist in each worker process).
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ database_url
+
+
+ str
+
+
+
+
Original database connection URL.
+
+
+
+ required
+
+
+
+
+ default_test_db
+
+
+ str
+
+
+
+
Suffix appended to the database name when
+PYTEST_XDIST_WORKER is not set.
+
+
+
+ required
+
+
+
+
+
+
+
Returns:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ str
+
+
+
+
A database URL with a worker- or default-specific database name.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Create and drop a per-worker database for pytest-xdist isolation.
+
Derives a worker-specific database URL using :func:worker_database_url,
+then delegates to :func:~fastapi_toolsets.db.create_database to create
+and drop it. Intended for use as a session-scoped fixture.
+
When running under xdist the database name is suffixed with the worker
+name (e.g. _gw0). Otherwise it is suffixed with default_test_db .
+
+
+
Parameters:
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+
+
+ database_url
+
+
+ str
+
+
+
+
Original database connection URL (used as the server
+connection and as the base for the worker database name).
+
+
+
+ required
+
+
+
+
+ default_test_db
+
+
+ str
+
+
+
+
Suffix appended to the database name when
+PYTEST_XDIST_WORKER is not set. Defaults to "test_db".
+
+
+
+ 'test_db'
+
+
+
+
+
+
+
Yields:
+
+
+
+ Type
+ Description
+
+
+
+
+
+ AsyncGenerator [str , None]
+
+
+
+
The worker-specific database URL.
+
+
+
+
+
+
+
+
+ Example
+ from fastapi_toolsets.pytest import create_worker_database , create_db_session
+
+DATABASE_URL = "postgresql+asyncpg://postgres:postgres@localhost/test_db"
+
+@pytest . fixture ( scope = "session" )
+async def worker_db_url ():
+ async with create_worker_database ( DATABASE_URL ) as url :
+ yield url
+
+@pytest . fixture
+async def db_session ( worker_db_url ):
+ async with create_db_session (
+ worker_db_url , Base , cleanup = True
+ ) as session :
+ yield session
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back to top
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v2.2/reference/schemas/index.html b/v2.2/reference/schemas/index.html
new file mode 100644
index 0000000..3de8194
--- /dev/null
+++ b/v2.2/reference/schemas/index.html
@@ -0,0 +1,2397 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ schemas - FastAPI Toolsets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Home
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Reference
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+schemas
+Here's the reference for all response models and types provided by the schemas module.
+You can import them directly from fastapi_toolsets.schemas:
+from fastapi_toolsets.schemas import (
+ PydanticBase ,
+ ResponseStatus ,
+ ApiError ,
+ BaseResponse ,
+ Response ,
+ ErrorResponse ,
+ OffsetPagination ,
+ CursorPagination ,
+ PaginatedResponse ,
+)
+
+
+
+
+
+
+
+
+
+
+
+
+ Bases: BaseModel
+
+
+
+
Base class for all Pydantic models with common configuration.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bases: str , Enum
+
+
+
+
Standard API response status.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bases: PydanticBase
+
+
+
+
Structured API error definition.
+
Used to define standard error responses with consistent format.
+
+
+
Attributes:
+
+
+
+ Name
+ Type
+ Description
+
+
+
+
+ code
+
+ int
+
+
+
+
+
+
+ msg
+
+ str
+
+
+
+
+
+
+ desc
+
+ str
+
+
+
+
Detailed error description
+
+
+
+
+ err_code
+
+ str
+
+
+
+
Application-specific error code (e.g., "AUTH-401")
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bases: PydanticBase
+
+
+
+
Base response structure for all API responses.
+
+
+
Attributes:
+
+
+
+ Name
+ Type
+ Description
+
+
+
+
+ status
+
+ ResponseStatus
+
+
+
+
+
+
+ message
+
+ str
+
+
+
+
Human-readable message
+
+
+
+
+ error_code
+
+ str | None
+
+
+
+
Error code if status is FAIL, None otherwise
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bases: BaseResponse , Generic [DataT ]
+
+
+
+
Generic API response with data payload.
+
+
+
+ Example
+ Response [ UserRead ]( data = user , message = "User retrieved" )
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bases: BaseResponse
+
+
+
+
Error response with additional description field.
+
Used for error responses that need more context.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bases: PydanticBase
+
+
+
+
Pagination metadata for offset-based list responses.
+
+
+
Attributes:
+
+
+
+ Name
+ Type
+ Description
+
+
+
+
+ total_count
+
+ int
+
+
+
+
Total number of items across all pages
+
+
+
+
+ items_per_page
+
+ int
+
+
+
+
Number of items per page
+
+
+
+
+ page
+
+ int
+
+
+
+
Current page number (1-indexed)
+
+
+
+
+ has_more
+
+ bool
+
+
+
+
Whether there are more pages
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bases: PydanticBase
+
+
+
+
Pagination metadata for cursor-based list responses.
+
+
+
Attributes:
+
+
+
+ Name
+ Type
+ Description
+
+
+
+
+ next_cursor
+
+ str | None
+
+
+
+
Encoded cursor for the next page, or None on the last page.
+
+
+
+
+ prev_cursor
+
+ str | None
+
+
+
+
Encoded cursor for the previous page, or None on the first page.
+
+
+
+
+ items_per_page
+
+ int
+
+
+
+
Number of items requested per page.
+
+
+
+
+ has_more
+
+ bool
+
+
+
+
Whether there is at least one more page after this one.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bases: BaseResponse , Generic [DataT ]
+
+
+
+
Paginated API response for list endpoints.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back to top
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v2.2/search.json b/v2.2/search.json
new file mode 100644
index 0000000..4b412ae
--- /dev/null
+++ b/v2.2/search.json
@@ -0,0 +1 @@
+{"config":{"separator":"[\\s\\-_,:!=\\[\\]()\\\\\"`/]+|\\.(?!\\d)"},"items":[{"location":"","level":1,"title":"FastAPI Toolsets","text":"A modular collection of production-ready utilities for FastAPI. Install only what you need — from async CRUD and database helpers to CLI tooling, Prometheus metrics, and pytest fixtures. Each module is independently installable via optional extras, keeping your dependency footprint minimal.
Documentation: https://fastapi-toolsets.d3vyce.fr
Source Code: https://github.com/d3vyce/fastapi-toolsets
","path":["FastAPI Toolsets"],"tags":[]},{"location":"#installation","level":2,"title":"Installation","text":"The base package includes the core modules (CRUD, database, schemas, exceptions, fixtures, dependencies, model mixins, logging):
uv add fastapi-toolsets\n Install only the extras you need:
uv add \"fastapi-toolsets[cli]\"\nuv add \"fastapi-toolsets[metrics]\"\nuv add \"fastapi-toolsets[pytest]\"\n Or install everything:
uv add \"fastapi-toolsets[all]\"\n ","path":["FastAPI Toolsets"],"tags":[]},{"location":"#features","level":2,"title":"Features","text":"","path":["FastAPI Toolsets"],"tags":[]},{"location":"#core","level":3,"title":"Core","text":" CRUD: Generic async CRUD operations with CrudFactory, built-in full-text/faceted search and Offset/Cursor pagination. Database: Session management, transaction helpers, table locking, and polling-based row change detection Dependencies: FastAPI dependency factories (PathDependency, BodyDependency) for automatic DB lookups from path or body parameters Fixtures: Fixture system with dependency management, context support, and pytest integration Model Mixins: SQLAlchemy mixins for common column patterns (UUIDMixin, CreatedAtMixin, UpdatedAtMixin, TimestampMixin) Standardized API Responses: Consistent response format with Response, PaginatedResponse, and PydanticBase Exception Handling: Structured error responses with automatic OpenAPI documentation Logging: Logging configuration with uvicorn integration via configure_logging and get_logger ","path":["FastAPI Toolsets"],"tags":[]},{"location":"#optional","level":3,"title":"Optional","text":" CLI: Django-like command-line interface with fixture management and custom commands support Metrics: Prometheus metrics endpoint with provider/collector registry Pytest Helpers: Async test client, database session management, pytest-xdist support, and table cleanup utilities ","path":["FastAPI Toolsets"],"tags":[]},{"location":"#license","level":2,"title":"License","text":"MIT License - see LICENSE for details.
","path":["FastAPI Toolsets"],"tags":[]},{"location":"#contributing","level":2,"title":"Contributing","text":"Contributions are welcome! Please feel free to submit issues and pull requests.
","path":["FastAPI Toolsets"],"tags":[]},{"location":"examples/pagination-search/","level":1,"title":"Pagination & search","text":"This example builds an articles listing endpoint that supports offset pagination, cursor pagination, full-text search, faceted filtering, and sorting — all from a single CrudFactory definition.
","path":["Examples","Pagination & search"],"tags":[]},{"location":"examples/pagination-search/#models","level":2,"title":"Models","text":"models.pyimport uuid\n\nfrom sqlalchemy import Boolean, ForeignKey, String, Text\nfrom sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship\n\nfrom fastapi_toolsets.models import CreatedAtMixin\n\n\nclass Base(DeclarativeBase):\n pass\n\n\nclass Category(Base):\n __tablename__ = \"categories\"\n\n id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)\n name: Mapped[str] = mapped_column(String(64), unique=True)\n\n articles: Mapped[list[\"Article\"]] = relationship(back_populates=\"category\")\n\n\nclass Article(Base, CreatedAtMixin):\n __tablename__ = \"articles\"\n\n id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)\n title: Mapped[str] = mapped_column(String(256))\n body: Mapped[str] = mapped_column(Text)\n status: Mapped[str] = mapped_column(String(32))\n published: Mapped[bool] = mapped_column(Boolean, default=False)\n category_id: Mapped[uuid.UUID | None] = mapped_column(\n ForeignKey(\"categories.id\"), nullable=True\n )\n\n category: Mapped[\"Category | None\"] = relationship(back_populates=\"articles\")\n ","path":["Examples","Pagination & search"],"tags":[]},{"location":"examples/pagination-search/#schemas","level":2,"title":"Schemas","text":"schemas.pyimport datetime\nimport uuid\n\nfrom fastapi_toolsets.schemas import PydanticBase\n\n\nclass ArticleRead(PydanticBase):\n id: uuid.UUID\n created_at: datetime.datetime\n title: str\n status: str\n published: bool\n category_id: uuid.UUID | None\n ","path":["Examples","Pagination & search"],"tags":[]},{"location":"examples/pagination-search/#crud","level":2,"title":"Crud","text":"Declare searchable_fields, facet_fields, and order_fields once on CrudFactory. All endpoints built from this class share the same defaults and can override them per call.
crud.pyfrom fastapi_toolsets.crud import CrudFactory\n\nfrom .models import Article, Category\n\nArticleCrud = CrudFactory(\n model=Article,\n cursor_column=Article.created_at,\n searchable_fields=[ # default fields for full-text search\n Article.title,\n Article.body,\n (Article.category, Category.name),\n ],\n facet_fields=[ # fields exposed as filter dropdowns\n Article.status,\n (Article.category, Category.name),\n ],\n order_fields=[ # fields exposed for client-driven ordering\n Article.title,\n Article.created_at,\n ],\n)\n ","path":["Examples","Pagination & search"],"tags":[]},{"location":"examples/pagination-search/#session-dependency","level":2,"title":"Session dependency","text":"db.pyfrom typing import Annotated\n\nfrom fastapi import Depends\nfrom sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine\n\nfrom fastapi_toolsets.db import create_db_context, create_db_dependency\n\nDATABASE_URL = \"postgresql+asyncpg://postgres:postgres@localhost:5432/postgres\"\n\nengine = create_async_engine(url=DATABASE_URL, future=True)\nasync_session_maker = async_sessionmaker(bind=engine, expire_on_commit=False)\n\nget_db = create_db_dependency(session_maker=async_session_maker)\nget_db_context = create_db_context(session_maker=async_session_maker)\n\n\nSessionDep = Annotated[AsyncSession, Depends(get_db)]\n Deploy a Postgres DB with docker
docker run -d --name postgres -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=postgres -p 5432:5432 postgres:18-alpine\n ","path":["Examples","Pagination & search"],"tags":[]},{"location":"examples/pagination-search/#app","level":2,"title":"App","text":"app.pyfrom fastapi import FastAPI\n\nfrom fastapi_toolsets.exceptions import init_exceptions_handlers\n\nfrom .routes import router\n\napp = FastAPI()\ninit_exceptions_handlers(app=app)\napp.include_router(router=router)\n ","path":["Examples","Pagination & search"],"tags":[]},{"location":"examples/pagination-search/#routes","level":2,"title":"Routes","text":"","path":["Examples","Pagination & search"],"tags":[]},{"location":"examples/pagination-search/#offset-pagination","level":3,"title":"Offset pagination","text":"Best for admin panels or any UI that needs a total item count and numbered pages.
routes.py:1:36from typing import Annotated\n\nfrom fastapi import APIRouter, Depends\n\nfrom fastapi_toolsets.crud import OrderByClause\nfrom fastapi_toolsets.schemas import (\n CursorPaginatedResponse,\n OffsetPaginatedResponse,\n PaginatedResponse,\n)\n\nfrom .crud import ArticleCrud\nfrom .db import SessionDep\nfrom .models import Article\nfrom .schemas import ArticleRead\n\nrouter = APIRouter(prefix=\"/articles\")\n\n\n@router.get(\"/offset\")\nasync def list_articles_offset(\n session: SessionDep,\n params: Annotated[\n dict,\n Depends(ArticleCrud.offset_params(default_page_size=20, max_page_size=100)),\n ],\n filter_by: Annotated[dict[str, list[str]], Depends(ArticleCrud.filter_params())],\n order_by: Annotated[\n OrderByClause | None,\n Depends(ArticleCrud.order_params(default_field=Article.created_at)),\n ],\n search: str | None = None,\n) -> OffsetPaginatedResponse[ArticleRead]:\n return await ArticleCrud.offset_paginate(\n session=session,\n **params,\n Example request
GET /articles/offset?page=2&items_per_page=10&search=fastapi&status=published&order_by=title&order=asc\n Example response
{\n \"status\": \"SUCCESS\",\n \"data\": [\n { \"id\": \"3f47ac69-...\", \"title\": \"FastAPI tips\", \"status\": \"published\", ... }\n ],\n \"pagination\": {\n \"total_count\": 42,\n \"page\": 2,\n \"items_per_page\": 10,\n \"has_more\": true\n },\n \"filter_attributes\": {\n \"status\": [\"archived\", \"draft\", \"published\"],\n \"name\": [\"backend\", \"frontend\", \"python\"]\n }\n}\n filter_attributes always reflects the values visible after applying the active filters. Use it to populate filter dropdowns on the client.
","path":["Examples","Pagination & search"],"tags":[]},{"location":"examples/pagination-search/#cursor-pagination","level":3,"title":"Cursor pagination","text":"Best for feeds, infinite scroll, or any high-throughput API where offset performance degrades.
routes.py:39:59 order_by=order_by,\n schema=ArticleRead,\n )\n\n\n@router.get(\"/cursor\")\nasync def list_articles_cursor(\n session: SessionDep,\n params: Annotated[\n dict,\n Depends(ArticleCrud.cursor_params(default_page_size=20, max_page_size=100)),\n ],\n filter_by: Annotated[dict[str, list[str]], Depends(ArticleCrud.filter_params())],\n order_by: Annotated[\n OrderByClause | None,\n Depends(ArticleCrud.order_params(default_field=Article.created_at)),\n ],\n search: str | None = None,\n) -> CursorPaginatedResponse[ArticleRead]:\n return await ArticleCrud.cursor_paginate(\n session=session,\n Example request
GET /articles/cursor?items_per_page=10&status=published&order_by=created_at&order=desc\n Example response
{\n \"status\": \"SUCCESS\",\n \"data\": [\n { \"id\": \"3f47ac69-...\", \"title\": \"FastAPI tips\", \"status\": \"published\", ... }\n ],\n \"pagination\": {\n \"next_cursor\": \"eyJ2YWx1ZSI6ICIzZjQ3YWM2OS0uLi4ifQ==\",\n \"prev_cursor\": null,\n \"items_per_page\": 10,\n \"has_more\": true\n },\n \"filter_attributes\": {\n \"status\": [\"published\"],\n \"name\": [\"backend\", \"python\"]\n }\n}\n Pass next_cursor as the cursor query parameter on the next request to advance to the next page.
","path":["Examples","Pagination & search"],"tags":[]},{"location":"examples/pagination-search/#search-behaviour","level":2,"title":"Search behaviour","text":"Both endpoints inherit the same searchable_fields declared on ArticleCrud:
Search is case-insensitive and uses a LIKE %query% pattern. Pass a SearchConfig instead of a plain string to control case sensitivity or switch to match_mode=\"all\" (AND across all fields instead of OR).
from fastapi_toolsets.crud import SearchConfig\n\n# Both title AND body must contain \"fastapi\"\nresult = await ArticleCrud.offset_paginate(\n session,\n search=SearchConfig(query=\"fastapi\", case_sensitive=True, match_mode=\"all\"),\n search_fields=[Article.title, Article.body],\n)\n ","path":["Examples","Pagination & search"],"tags":[]},{"location":"migration/v2/","level":1,"title":"Migrating to v2.0","text":"This page covers every breaking change introduced in v2.0 and the steps required to update your code.
","path":["Migration","Migrating to v2.0"],"tags":[]},{"location":"migration/v2/#crud","level":2,"title":"CRUD","text":"","path":["Migration","Migrating to v2.0"],"tags":[]},{"location":"migration/v2/#schema-is-now-required-in-offset_paginate-and-cursor_paginate","level":3,"title":"schema is now required in offset_paginate() and cursor_paginate()","text":"Calls that omit schema will now raise a TypeError at runtime.
Previously schema was optional; omitting it returned raw SQLAlchemy model instances inside the response. It is now a required keyword argument and the response always contains serialized schema instances.
Before (v1)Now (v2) # schema omitted — returned raw model instances\nresult = await UserCrud.offset_paginate(session=session, page=1)\nresult = await UserCrud.cursor_paginate(session=session, cursor=token)\n result = await UserCrud.offset_paginate(session=session, page=1, schema=UserRead)\nresult = await UserCrud.cursor_paginate(session=session, cursor=token, schema=UserRead)\n ","path":["Migration","Migrating to v2.0"],"tags":[]},{"location":"migration/v2/#as_response-removed-from-create-get-and-update","level":3,"title":"as_response removed from create(), get(), and update()","text":"Passing as_response to these methods will raise a TypeError at runtime.
The as_response=True shorthand is replaced by passing a schema directly. The return value is a Response[schema] when schema is provided, or the raw model instance when it is not.
Before (v1)Now (v2) user = await UserCrud.create(session=session, obj=data, as_response=True)\nuser = await UserCrud.get(session=session, filters=filters, as_response=True)\nuser = await UserCrud.update(session=session, obj=data, filters, as_response=True)\n user = await UserCrud.create(session=session, obj=data, schema=UserRead)\nuser = await UserCrud.get(session=session, filters=filters, schema=UserRead)\nuser = await UserCrud.update(session=session, obj=data, filters, schema=UserRead)\n ","path":["Migration","Migrating to v2.0"],"tags":[]},{"location":"migration/v2/#delete-as_response-renamed-and-return-type-changed","level":3,"title":"delete(): as_response renamed and return type changed","text":"as_response is gone, and the plain (non-response) call no longer returns True.
Two changes were made to delete():
The as_response parameter is renamed to return_response. When called without return_response=True, the method now returns None on success instead of True. Before (v1)Now (v2) ok = await UserCrud.delete(session=session, filters=filters)\nif ok: # True on success\n ...\n\nresponse = await UserCrud.delete(session=session, filters=filters, as_response=True)\n await UserCrud.delete(session=session, filters=filters) # returns None\n\nresponse = await UserCrud.delete(session=session, filters=filters, return_response=True)\n ","path":["Migration","Migrating to v2.0"],"tags":[]},{"location":"migration/v2/#paginate-alias-removed","level":3,"title":"paginate() alias removed","text":"Any call to crud.paginate(...) will raise AttributeError at runtime.
The paginate shorthand was an alias for offset_paginate. It has been removed; call offset_paginate directly.
Before (v1)Now (v2) result = await UserCrud.paginate(session=session, page=2, items_per_page=20, schema=UserRead)\n result = await UserCrud.offset_paginate(session=session, page=2, items_per_page=20, schema=UserRead)\n ","path":["Migration","Migrating to v2.0"],"tags":[]},{"location":"migration/v2/#exceptions","level":2,"title":"Exceptions","text":"","path":["Migration","Migrating to v2.0"],"tags":[]},{"location":"migration/v2/#missing-api_error-raises-typeerror-at-class-definition-time","level":3,"title":"Missing api_error raises TypeError at class definition time","text":"Unfinished or stub exception subclasses that previously compiled fine will now fail on import.
In v1, a subclass without api_error would only fail when the exception was raised. In v2, __init_subclass__ validates this at class definition time.
Before (v1)Now (v2) class MyError(ApiException):\n pass # fine until raised\n class MyError(ApiException):\n pass # TypeError: MyError must define an 'api_error' class attribute.\n For shared base classes that are not meant to be raised directly, use abstract=True:
class BillingError(ApiException, abstract=True):\n \"\"\"Base for all billing-related errors — not raised directly.\"\"\"\n\nclass PaymentRequiredError(BillingError):\n api_error = ApiError(code=402, msg=\"Payment Required\", desc=\"...\", err_code=\"BILLING-402\")\n ","path":["Migration","Migrating to v2.0"],"tags":[]},{"location":"migration/v2/#schemas","level":2,"title":"Schemas","text":"","path":["Migration","Migrating to v2.0"],"tags":[]},{"location":"migration/v2/#pagination-alias-removed","level":3,"title":"Pagination alias removed","text":"Pagination was already deprecated in v1 and is fully removed in v2, you now need to use OffsetPagination or CursorPagination.
","path":["Migration","Migrating to v2.0"],"tags":[]},{"location":"module/cli/","level":1,"title":"CLI","text":"Typer-based command-line interface for managing your FastAPI application, with built-in fixture commands integration.
","path":["Modules","CLI"],"tags":[]},{"location":"module/cli/#installation","level":2,"title":"Installation","text":"uvpip uv add \"fastapi-toolsets[cli]\"\n pip install \"fastapi-toolsets[cli]\"\n ","path":["Modules","CLI"],"tags":[]},{"location":"module/cli/#overview","level":2,"title":"Overview","text":"The cli module provides a manager entry point built with Typer. It allow custom commands to be added in addition of the fixture commands when a FixtureRegistry and a database context are configured.
","path":["Modules","CLI"],"tags":[]},{"location":"module/cli/#configuration","level":2,"title":"Configuration","text":"Configure the CLI in your pyproject.toml:
[tool.fastapi-toolsets]\ncli = \"myapp.cli:cli\" # Custom Typer app\nfixtures = \"myapp.fixtures:registry\" # FixtureRegistry instance\ndb_context = \"myapp.db:db_context\" # Async context manager for sessions\n All fields are optional. Without configuration, the manager command still works but no command are available.
","path":["Modules","CLI"],"tags":[]},{"location":"module/cli/#usage","level":2,"title":"Usage","text":"# Manager commands\nmanager --help\n\n Usage: manager [OPTIONS] COMMAND [ARGS]...\n\n FastAPI utilities CLI.\n\n╭─ Options ────────────────────────────────────────────────────────────────────────╮\n│ --install-completion Install completion for the current shell. │\n│ --show-completion Show completion for the current shell, to copy it │\n│ or customize the installation. │\n│ --help Show this message and exit. │\n╰──────────────────────────────────────────────────────────────────────────────────╯\n╭─ Commands ───────────────────────────────────────────────────────────────────────╮\n│ check-db │\n│ fixtures Manage database fixtures. │\n╰──────────────────────────────────────────────────────────────────────────────────╯\n\n# Fixtures commands\nmanager fixtures --help\n\n Usage: manager fixtures [OPTIONS] COMMAND [ARGS]...\n\n Manage database fixtures.\n\n╭─ Options ────────────────────────────────────────────────────────────────────────╮\n│ --help Show this message and exit. │\n╰──────────────────────────────────────────────────────────────────────────────────╯\n╭─ Commands ───────────────────────────────────────────────────────────────────────╮\n│ list List all registered fixtures. │\n│ load Load fixtures into the database. │\n╰──────────────────────────────────────────────────────────────────────────────────╯\n ","path":["Modules","CLI"],"tags":[]},{"location":"module/cli/#custom-cli","level":2,"title":"Custom CLI","text":"You can extend the CLI by providing your own Typer app. The manager entry point will merge your app's commands with the built-in ones:
# myapp/cli.py\nimport typer\n\ncli = typer.Typer()\n\n@cli.command()\ndef hello():\n print(\"Hello from my app!\")\n [tool.fastapi-toolsets]\ncli = \"myapp.cli:cli\"\n API Reference
","path":["Modules","CLI"],"tags":[]},{"location":"module/crud/","level":1,"title":"CRUD","text":"Generic async CRUD operations for SQLAlchemy models with search, pagination, and many-to-many support.
Info
This module has been coded and tested to be compatible with PostgreSQL only.
","path":["Modules","CRUD"],"tags":[]},{"location":"module/crud/#overview","level":2,"title":"Overview","text":"The crud module provides AsyncCrud, an abstract base class with a full suite of async database operations, and CrudFactory, a convenience function to instantiate it for a given model.
","path":["Modules","CRUD"],"tags":[]},{"location":"module/crud/#creating-a-crud-class","level":2,"title":"Creating a CRUD class","text":"from fastapi_toolsets.crud import CrudFactory\nfrom myapp.models import User\n\nUserCrud = CrudFactory(model=User)\n CrudFactory dynamically creates a class named AsyncUserCrud with User as its model.
","path":["Modules","CRUD"],"tags":[]},{"location":"module/crud/#basic-operations","level":2,"title":"Basic operations","text":"get_or_none added in v2.2
# Create\nuser = await UserCrud.create(session=session, obj=UserCreateSchema(username=\"alice\"))\n\n# Get one (raises NotFoundError if not found)\nuser = await UserCrud.get(session=session, filters=[User.id == user_id])\n\n# Get one or None (never raises)\nuser = await UserCrud.get_or_none(session=session, filters=[User.id == user_id])\n\n# Get first or None\nuser = await UserCrud.first(session=session, filters=[User.email == email])\n\n# Get multiple\nusers = await UserCrud.get_multi(session=session, filters=[User.is_active == True])\n\n# Update\nuser = await UserCrud.update(session=session, obj=UserUpdateSchema(username=\"bob\"), filters=[User.id == user_id])\n\n# Delete\nawait UserCrud.delete(session=session, filters=[User.id == user_id])\n\n# Count / exists\ncount = await UserCrud.count(session=session, filters=[User.is_active == True])\nexists = await UserCrud.exists(session=session, filters=[User.email == email])\n ","path":["Modules","CRUD"],"tags":[]},{"location":"module/crud/#fetching-a-single-record","level":2,"title":"Fetching a single record","text":"Three methods fetch a single record — choose based on how you want to handle the \"not found\" case and whether you need strict uniqueness:
Method Not found Multiple results get raises NotFoundError raises MultipleResultsFound get_or_none returns None raises MultipleResultsFound first returns None returns the first match silently Use get when the record must exist (e.g. a detail endpoint that should return 404):
user = await UserCrud.get(session=session, filters=[User.id == user_id])\n Use get_or_none when the record may not exist but you still want strict uniqueness enforcement:
user = await UserCrud.get_or_none(session=session, filters=[User.email == email])\nif user is None:\n ... # handle missing case without catching an exception\n Use first when you only care about any one match and don't need uniqueness:
user = await UserCrud.first(session=session, filters=[User.is_active == True])\n ","path":["Modules","CRUD"],"tags":[]},{"location":"module/crud/#pagination","level":2,"title":"Pagination","text":"Added in v1.1 (only offset_pagination via paginate if <v1.1)
Two pagination strategies are available. Both return a PaginatedResponse but differ in how they navigate through results.
offset_paginate cursor_paginate Total count Yes No Jump to arbitrary page Yes No Performance on deep pages Degrades Constant Stable under concurrent inserts No Yes Search compatible Yes Yes Use case Admin panels, numbered pagination Feeds, APIs, infinite scroll","path":["Modules","CRUD"],"tags":[]},{"location":"module/crud/#offset-pagination","level":3,"title":"Offset pagination","text":"@router.get(\n \"\",\n response_model=PaginatedResponse[User],\n)\nasync def get_users(\n session: SessionDep,\n items_per_page: int = 50,\n page: int = 1,\n):\n return await crud.UserCrud.offset_paginate(\n session=session,\n items_per_page=items_per_page,\n page=page,\n )\n The offset_paginate method returns a PaginatedResponse whose pagination field is an OffsetPagination object:
{\n \"status\": \"SUCCESS\",\n \"data\": [\"...\"],\n \"pagination\": {\n \"total_count\": 100,\n \"page\": 1,\n \"items_per_page\": 20,\n \"has_more\": true\n }\n}\n ","path":["Modules","CRUD"],"tags":[]},{"location":"module/crud/#cursor-pagination","level":3,"title":"Cursor pagination","text":"@router.get(\n \"\",\n response_model=PaginatedResponse[UserRead],\n)\nasync def list_users(\n session: SessionDep,\n cursor: str | None = None,\n items_per_page: int = 20,\n):\n return await UserCrud.cursor_paginate(\n session=session,\n cursor=cursor,\n items_per_page=items_per_page,\n )\n The cursor_paginate method returns a PaginatedResponse whose pagination field is a CursorPagination object:
{\n \"status\": \"SUCCESS\",\n \"data\": [\"...\"],\n \"pagination\": {\n \"next_cursor\": \"eyJ2YWx1ZSI6ICIzZjQ3YWM2OS0uLi4ifQ==\",\n \"prev_cursor\": null,\n \"items_per_page\": 20,\n \"has_more\": true\n }\n}\n Pass next_cursor as the cursor query parameter on the next request to advance to the next page. prev_cursor is set on pages 2+ and points back to the first item of the current page. Both are null when there is no adjacent page.
","path":["Modules","CRUD"],"tags":[]},{"location":"module/crud/#choosing-a-cursor-column","level":4,"title":"Choosing a cursor column","text":"The cursor column is set once on CrudFactory via the cursor_column parameter. It must be monotonically ordered for stable results:
Auto-increment integer PKs UUID v7 PKs Timestamps Warning
Random UUID v4 PKs are not suitable as cursor columns because their ordering is non-deterministic.
Note
cursor_column is required. Calling cursor_paginate on a CRUD class that has no cursor_column configured raises a ValueError.
The cursor value is base64-encoded when returned to the client and decoded back to the correct Python type on the next request. The following SQLAlchemy column types are supported:
SQLAlchemy type Python type Integer, BigInteger, SmallInteger int Uuid uuid.UUID DateTime datetime.datetime Date datetime.date Float, Numeric decimal.Decimal # Paginate by the primary key\nPostCrud = CrudFactory(model=Post, cursor_column=Post.id)\n\n# Paginate by a timestamp column instead\nPostCrud = CrudFactory(model=Post, cursor_column=Post.created_at)\n ","path":["Modules","CRUD"],"tags":[]},{"location":"module/crud/#search","level":2,"title":"Search","text":"Two search strategies are available, both compatible with offset_paginate and cursor_paginate.
Full-text search Faceted search Input Free-text string Exact column values Relationship support Yes Yes Use case Search bars Filter dropdowns You can use both search strategies in the same endpoint!
","path":["Modules","CRUD"],"tags":[]},{"location":"module/crud/#full-text-search","level":3,"title":"Full-text search","text":"Added in v2.2.1
The model's primary key is always included in searchable_fields automatically, so searching by ID works out of the box without any configuration. When no searchable_fields are declared, only the primary key is searched.
Declare searchable_fields on the CRUD class. Relationship traversal is supported via tuples:
PostCrud = CrudFactory(\n model=Post,\n searchable_fields=[\n Post.title,\n Post.content,\n (Post.author, User.username), # search across relationship\n ],\n)\n You can override searchable_fields per call with search_fields:
result = await UserCrud.offset_paginate(\n session=session,\n search_fields=[User.country],\n)\n This allows searching with both offset_paginate and cursor_paginate:
@router.get(\n \"\",\n response_model=PaginatedResponse[User],\n)\nasync def get_users(\n session: SessionDep,\n items_per_page: int = 50,\n page: int = 1,\n search: str | None = None,\n):\n return await crud.UserCrud.offset_paginate(\n session=session,\n items_per_page=items_per_page,\n page=page,\n search=search,\n )\n @router.get(\n \"\",\n response_model=PaginatedResponse[User],\n)\nasync def get_users(\n session: SessionDep,\n cursor: str | None = None,\n items_per_page: int = 50,\n search: str | None = None,\n):\n return await crud.UserCrud.cursor_paginate(\n session=session,\n items_per_page=items_per_page,\n cursor=cursor,\n search=search,\n )\n ","path":["Modules","CRUD"],"tags":[]},{"location":"module/crud/#faceted-search","level":3,"title":"Faceted search","text":"Added in v1.2
Declare facet_fields on the CRUD class to return distinct column values alongside paginated results. This is useful for populating filter dropdowns or building faceted search UIs.
Facet fields use the same syntax as searchable_fields — direct columns or relationship tuples:
UserCrud = CrudFactory(\n model=User,\n facet_fields=[\n User.status,\n User.country,\n (User.role, Role.name), # value from a related model\n ],\n)\n You can override facet_fields per call:
result = await UserCrud.offset_paginate(\n session=session,\n facet_fields=[User.country],\n)\n The distinct values are returned in the filter_attributes field of PaginatedResponse:
{\n \"status\": \"SUCCESS\",\n \"data\": [\"...\"],\n \"pagination\": { \"...\" },\n \"filter_attributes\": {\n \"status\": [\"active\", \"inactive\"],\n \"country\": [\"DE\", \"FR\", \"US\"],\n \"name\": [\"admin\", \"editor\", \"viewer\"]\n }\n}\n Use filter_by to pass the client's chosen filter values directly — no need to build SQLAlchemy conditions by hand. Any unknown key raises InvalidFacetFilterError.
The keys in filter_by are the same keys the client received in filter_attributes.
Keys are normally the terminal column.key (e.g. \"name\" for Role.name). When two facet fields share the same column key (e.g. (Build.project, Project.name) and (Build.os, Os.name)), the relationship name is prepended automatically: \"project__name\" and \"os__name\".
filter_by and filters can be combined — both are applied with AND logic.
Use filter_params() to generate a dict with the facet filter values from the query parameters:
from typing import Annotated\n\nfrom fastapi import Depends\n\nUserCrud = CrudFactory(\n model=User,\n facet_fields=[User.status, User.country, (User.role, Role.name)],\n)\n\n@router.get(\"\", response_model_exclude_none=True)\nasync def list_users(\n session: SessionDep,\n page: int = 1,\n filter_by: Annotated[dict[str, list[str]], Depends(UserCrud.filter_params())],\n) -> PaginatedResponse[UserRead]:\n return await UserCrud.offset_paginate(\n session=session,\n page=page,\n filter_by=filter_by,\n )\n Both single-value and multi-value query parameters work:
GET /users?status=active → filter_by={\"status\": [\"active\"]}\nGET /users?status=active&country=FR → filter_by={\"status\": [\"active\"], \"country\": [\"FR\"]}\nGET /users?role=admin&role=editor → filter_by={\"role\": [\"admin\", \"editor\"]} (IN clause)\n ","path":["Modules","CRUD"],"tags":[]},{"location":"module/crud/#sorting","level":2,"title":"Sorting","text":"Added in v1.3
Declare order_fields on the CRUD class to expose client-driven column ordering via order_by and order query parameters.
UserCrud = CrudFactory(\n model=User,\n order_fields=[\n User.name,\n User.created_at,\n ],\n)\n Call order_params() to generate a FastAPI dependency that maps the query parameters to an OrderByClause expression:
from typing import Annotated\n\nfrom fastapi import Depends\nfrom fastapi_toolsets.crud import OrderByClause\n\n@router.get(\"\")\nasync def list_users(\n session: SessionDep,\n order_by: Annotated[OrderByClause | None, Depends(UserCrud.order_params())],\n) -> PaginatedResponse[UserRead]:\n return await UserCrud.offset_paginate(session=session, order_by=order_by)\n The dependency adds two query parameters to the endpoint:
Parameter Type order_by str | null order asc or desc GET /users?order_by=name&order=asc → ORDER BY users.name ASC\nGET /users?order_by=name&order=desc → ORDER BY users.name DESC\n An unknown order_by value raises InvalidOrderFieldError (HTTP 422).
You can also pass order_fields directly to order_params() to override the class-level defaults without modifying them:
UserOrderParams = UserCrud.order_params(order_fields=[User.name])\n ","path":["Modules","CRUD"],"tags":[]},{"location":"module/crud/#relationship-loading","level":2,"title":"Relationship loading","text":"Added in v1.1
By default, SQLAlchemy relationships are not loaded unless explicitly requested. Instead of using lazy=\"selectin\" on model definitions (which is implicit and applies globally), define a default_load_options on the CRUD class to control loading strategy explicitly.
Warning
Avoid using lazy=\"selectin\" on model relationships. It fires silently on every query, cannot be disabled per-call, and can cause unexpected cascading loads through deep relationship chains. Use default_load_options instead.
from sqlalchemy.orm import selectinload\n\nArticleCrud = CrudFactory(\n model=Article,\n default_load_options=[\n selectinload(Article.category),\n selectinload(Article.tags),\n ],\n)\n default_load_options applies automatically to all read operations (get, first, get_multi, offset_paginate, cursor_paginate). When load_options is passed at call-site, it fully replaces default_load_options for that query — giving you precise per-call control:
# Only loads category, tags are not loaded\narticle = await ArticleCrud.get(\n session=session,\n filters=[Article.id == article_id],\n load_options=[selectinload(Article.category)],\n)\n\n# Loads nothing — useful for write-then-refresh flows or lightweight checks\narticles = await ArticleCrud.get_multi(session=session, load_options=[])\n ","path":["Modules","CRUD"],"tags":[]},{"location":"module/crud/#many-to-many-relationships","level":2,"title":"Many-to-many relationships","text":"Use m2m_fields to map schema fields containing lists of IDs to SQLAlchemy relationships. The CRUD class resolves and validates all IDs before persisting:
PostCrud = CrudFactory(\n model=Post,\n m2m_fields={\"tag_ids\": Post.tags},\n)\n\npost = await PostCrud.create(session=session, obj=PostCreateSchema(title=\"Hello\", tag_ids=[1, 2, 3]))\n ","path":["Modules","CRUD"],"tags":[]},{"location":"module/crud/#upsert","level":2,"title":"Upsert","text":"Atomic INSERT ... ON CONFLICT DO UPDATE using PostgreSQL:
await UserCrud.upsert(\n session=session,\n obj=UserCreateSchema(email=\"alice@example.com\", username=\"alice\"),\n index_elements=[User.email],\n set_={\"username\"},\n)\n ","path":["Modules","CRUD"],"tags":[]},{"location":"module/crud/#response-serialization","level":2,"title":"Response serialization","text":"Added in v1.1
Pass a Pydantic schema class to create, get, update, or offset_paginate to serialize the result directly into that schema and wrap it in a Response[schema] or PaginatedResponse[schema]:
class UserRead(PydanticBase):\n id: UUID\n username: str\n\n@router.get(\n \"/{uuid}\",\n responses=generate_error_responses(NotFoundError),\n)\nasync def get_user(session: SessionDep, uuid: UUID) -> Response[UserRead]:\n return await crud.UserCrud.get(\n session=session,\n filters=[User.id == uuid],\n schema=UserRead,\n )\n\n@router.get(\"\")\nasync def list_users(session: SessionDep, page: int = 1) -> PaginatedResponse[UserRead]:\n return await crud.UserCrud.offset_paginate(\n session=session,\n page=page,\n schema=UserRead,\n )\n The schema must have from_attributes=True (or inherit from PydanticBase) so it can be built from SQLAlchemy model instances.
API Reference
","path":["Modules","CRUD"],"tags":[]},{"location":"module/db/","level":1,"title":"DB","text":"SQLAlchemy async session management with transactions, table locking, and row-change polling.
Info
This module has been coded and tested to be compatible with PostgreSQL only.
","path":["Modules","DB"],"tags":[]},{"location":"module/db/#overview","level":2,"title":"Overview","text":"The db module provides helpers to create FastAPI dependencies and context managers for AsyncSession, along with utilities for nested transactions, table lock and polling for row changes.
","path":["Modules","DB"],"tags":[]},{"location":"module/db/#session-dependency","level":2,"title":"Session dependency","text":"Use create_db_dependency to create a FastAPI dependency that yields a session and auto-commits on success:
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker\nfrom fastapi_toolsets.db import create_db_dependency\n\nengine = create_async_engine(url=\"postgresql+asyncpg://...\", future=True)\nsession_maker = async_sessionmaker(bind=engine, expire_on_commit=False)\n\nget_db = create_db_dependency(session_maker=session_maker)\n\n@router.get(\"/users\")\nasync def list_users(session: AsyncSession = Depends(get_db)):\n ...\n ","path":["Modules","DB"],"tags":[]},{"location":"module/db/#session-context-manager","level":2,"title":"Session context manager","text":"Use create_db_context for sessions outside request handlers (e.g. background tasks, CLI commands):
from fastapi_toolsets.db import create_db_context\n\ndb_context = create_db_context(session_maker=session_maker)\n\nasync def seed():\n async with db_context() as session:\n ...\n ","path":["Modules","DB"],"tags":[]},{"location":"module/db/#nested-transactions","level":2,"title":"Nested transactions","text":"get_transaction handles savepoints automatically, allowing safe nesting:
from fastapi_toolsets.db import get_transaction\n\nasync def create_user_with_role(session=session):\n async with get_transaction(session=session):\n ...\n async with get_transaction(session=session): # uses savepoint\n ...\n ","path":["Modules","DB"],"tags":[]},{"location":"module/db/#table-locking","level":2,"title":"Table locking","text":"lock_tables acquires PostgreSQL table-level locks before executing critical sections:
from fastapi_toolsets.db import lock_tables\n\nasync with lock_tables(session=session, tables=[User], mode=\"EXCLUSIVE\"):\n # No other transaction can modify User until this block exits\n ...\n Available lock modes are defined in LockMode: ACCESS_SHARE, ROW_SHARE, ROW_EXCLUSIVE, SHARE_UPDATE_EXCLUSIVE, SHARE, SHARE_ROW_EXCLUSIVE, EXCLUSIVE, ACCESS_EXCLUSIVE.
","path":["Modules","DB"],"tags":[]},{"location":"module/db/#row-change-polling","level":2,"title":"Row-change polling","text":"wait_for_row_change polls a row until a specific column changes value, useful for waiting on async side effects:
from fastapi_toolsets.db import wait_for_row_change\n\n# Wait up to 30s for order.status to change\nawait wait_for_row_change(\n session=session,\n model=Order,\n pk_value=order_id,\n columns=[Order.status],\n interval=1.0,\n timeout=30.0,\n)\n ","path":["Modules","DB"],"tags":[]},{"location":"module/db/#creating-a-database","level":2,"title":"Creating a database","text":"Added in v2.1
create_database creates a database at a given URL. It connects to server_url and issues a CREATE DATABASE statement:
from fastapi_toolsets.db import create_database\n\nSERVER_URL = \"postgresql+asyncpg://postgres:postgres@localhost/postgres\"\n\nawait create_database(db_name=\"myapp_test\", server_url=SERVER_URL)\n For test isolation with automatic cleanup, use create_worker_database from the pytest module instead — it handles drop-before, create, and drop-after automatically.
","path":["Modules","DB"],"tags":[]},{"location":"module/db/#cleaning-up-tables","level":2,"title":"Cleaning up tables","text":"Added in v2.1
cleanup_tables truncates all tables:
from fastapi_toolsets.db import cleanup_tables\n\n@pytest.fixture(autouse=True)\nasync def clean(db_session):\n yield\n await cleanup_tables(session=db_session, base=Base)\n API Reference
","path":["Modules","DB"],"tags":[]},{"location":"module/dependencies/","level":1,"title":"Dependencies","text":"FastAPI dependency factories for automatic model resolution from path and body parameters.
","path":["Modules","Dependencies"],"tags":[]},{"location":"module/dependencies/#overview","level":2,"title":"Overview","text":"The dependencies module provides two factory functions that create FastAPI dependencies to fetch a model instance from the database automatically — either from a path parameter or from a request body field — and inject it directly into your route handler.
","path":["Modules","Dependencies"],"tags":[]},{"location":"module/dependencies/#pathdependency","level":2,"title":"PathDependency","text":"PathDependency resolves a model from a URL path parameter and injects it into the route handler. Raises NotFoundError automatically if the record does not exist.
from fastapi_toolsets.dependencies import PathDependency\n\nUserDep = PathDependency(model=User, field=User.id, session_dep=get_db)\n\n@router.get(\"/users/{user_id}\")\nasync def get_user(user: User = UserDep):\n return user\n By default the parameter name is inferred from the field (user_id for User.id). You can override it:
UserDep = PathDependency(model=User, field=User.id, session_dep=get_db, param_name=\"id\")\n\n@router.get(\"/users/{id}\")\nasync def get_user(user: User = UserDep):\n return user\n ","path":["Modules","Dependencies"],"tags":[]},{"location":"module/dependencies/#bodydependency","level":2,"title":"BodyDependency","text":"BodyDependency resolves a model from a field in the request body. Useful when a body contains a foreign key and you want the full object injected:
from fastapi_toolsets.dependencies import BodyDependency\n\nRoleDep = BodyDependency(model=Role, field=Role.id, session_dep=get_db, body_field=\"role_id\")\n\n@router.post(\"/users\")\nasync def create_user(body: UserCreateSchema, role: Role = RoleDep):\n user = User(username=body.username, role=role)\n ...\n API Reference
","path":["Modules","Dependencies"],"tags":[]},{"location":"module/exceptions/","level":1,"title":"Exceptions","text":"Structured API exceptions with consistent error responses and automatic OpenAPI documentation.
","path":["Modules","Exceptions"],"tags":[]},{"location":"module/exceptions/#overview","level":2,"title":"Overview","text":"The exceptions module provides a set of pre-built HTTP exceptions and a FastAPI exception handler that formats all errors — including validation errors — into a uniform ErrorResponse.
","path":["Modules","Exceptions"],"tags":[]},{"location":"module/exceptions/#setup","level":2,"title":"Setup","text":"Register the exception handlers on your FastAPI app at startup:
from fastapi import FastAPI\nfrom fastapi_toolsets.exceptions import init_exceptions_handlers\n\napp = FastAPI()\ninit_exceptions_handlers(app=app)\n This registers handlers for:
ApiException — all custom exceptions below HTTPException — Starlette/FastAPI HTTP errors RequestValidationError — Pydantic request validation (422) ResponseValidationError — Pydantic response validation (422) Exception — unhandled errors (500) It also patches app.openapi() to replace the default Pydantic 422 schema with a structured example matching the ErrorResponse format.
","path":["Modules","Exceptions"],"tags":[]},{"location":"module/exceptions/#built-in-exceptions","level":2,"title":"Built-in exceptions","text":"Exception Status Default message UnauthorizedError 401 Unauthorized ForbiddenError 403 Forbidden NotFoundError 404 Not Found ConflictError 409 Conflict NoSearchableFieldsError 400 No Searchable Fields InvalidFacetFilterError 400 Invalid Facet Filter InvalidOrderFieldError 422 Invalid Order Field","path":["Modules","Exceptions"],"tags":[]},{"location":"module/exceptions/#per-instance-overrides","level":3,"title":"Per-instance overrides","text":"All built-in exceptions accept optional keyword arguments to customise the response for a specific raise site without changing the class defaults:
Argument Effect detail Overrides both str(exc) (log output) and the message field in the response body desc Overrides the description field data Overrides the data field raise NotFoundError(detail=\"User 42 not found\", desc=\"No user with that ID exists in the database.\")\n ","path":["Modules","Exceptions"],"tags":[]},{"location":"module/exceptions/#custom-exceptions","level":2,"title":"Custom exceptions","text":"Subclass ApiException and define an api_error class variable:
from fastapi_toolsets.exceptions import ApiException\nfrom fastapi_toolsets.schemas import ApiError\n\nclass PaymentRequiredError(ApiException):\n api_error = ApiError(\n code=402,\n msg=\"Payment Required\",\n desc=\"Your subscription has expired.\",\n err_code=\"BILLING-402\",\n )\n Warning
Subclasses that do not define api_error raise a TypeError at class creation time, not at raise time.
","path":["Modules","Exceptions"],"tags":[]},{"location":"module/exceptions/#custom-__init__","level":3,"title":"Custom __init__","text":"Override __init__ to compute detail, desc, or data dynamically, then delegate to super().__init__():
class OrderValidationError(ApiException):\n api_error = ApiError(\n code=422,\n msg=\"Order Validation Failed\",\n desc=\"One or more order fields are invalid.\",\n err_code=\"ORDER-422\",\n )\n\n def __init__(self, *field_errors: str) -> None:\n super().__init__(\n f\"{len(field_errors)} validation error(s)\",\n desc=\", \".join(field_errors),\n data={\"errors\": [{\"message\": e} for e in field_errors]},\n )\n ","path":["Modules","Exceptions"],"tags":[]},{"location":"module/exceptions/#intermediate-base-classes","level":3,"title":"Intermediate base classes","text":"Use abstract=True when creating a shared base that is not meant to be raised directly:
class BillingError(ApiException, abstract=True):\n \"\"\"Base for all billing-related errors.\"\"\"\n\nclass PaymentRequiredError(BillingError):\n api_error = ApiError(code=402, msg=\"Payment Required\", desc=\"...\", err_code=\"BILLING-402\")\n\nclass SubscriptionExpiredError(BillingError):\n api_error = ApiError(code=402, msg=\"Subscription Expired\", desc=\"...\", err_code=\"BILLING-402-EXP\")\n ","path":["Modules","Exceptions"],"tags":[]},{"location":"module/exceptions/#openapi-response-documentation","level":2,"title":"OpenAPI response documentation","text":"Use generate_error_responses to add error schemas to your endpoint's OpenAPI spec:
from fastapi_toolsets.exceptions import generate_error_responses, NotFoundError, ForbiddenError\n\n@router.get(\n \"/users/{id}\",\n responses=generate_error_responses(NotFoundError, ForbiddenError),\n)\nasync def get_user(...): ...\n Multiple exceptions sharing the same HTTP status code are grouped under one entry, each appearing as a named example keyed by its err_code. This keeps the OpenAPI UI readable when several error variants map to the same status.
API Reference
","path":["Modules","Exceptions"],"tags":[]},{"location":"module/fixtures/","level":1,"title":"Fixtures","text":"Dependency-aware database seeding with context-based loading strategies.
","path":["Modules","Fixtures"],"tags":[]},{"location":"module/fixtures/#overview","level":2,"title":"Overview","text":"The fixtures module lets you define named fixtures with dependencies between them, then load them into the database in the correct order. Fixtures can be scoped to contexts (e.g. base data, testing data) so that only the relevant ones are loaded for each environment.
","path":["Modules","Fixtures"],"tags":[]},{"location":"module/fixtures/#defining-fixtures","level":2,"title":"Defining fixtures","text":"from fastapi_toolsets.fixtures import FixtureRegistry, Context\n\nfixtures = FixtureRegistry()\n\n@fixtures.register\ndef roles():\n return [\n Role(id=1, name=\"admin\"),\n Role(id=2, name=\"user\"),\n ]\n\n@fixtures.register(depends_on=[\"roles\"], contexts=[Context.TESTING])\ndef test_users():\n return [\n User(id=1, username=\"alice\", role_id=1),\n User(id=2, username=\"bob\", role_id=2),\n ]\n Dependencies declared via depends_on are resolved topologically — roles will always be loaded before test_users.
","path":["Modules","Fixtures"],"tags":[]},{"location":"module/fixtures/#loading-fixtures","level":2,"title":"Loading fixtures","text":"By context with load_fixtures_by_context:
from fastapi_toolsets.fixtures import load_fixtures_by_context\n\nasync with db_context() as session:\n await load_fixtures_by_context(session=session, registry=fixtures, context=Context.TESTING)\n Directly with load_fixtures:
from fastapi_toolsets.fixtures import load_fixtures\n\nasync with db_context() as session:\n await load_fixtures(session=session, registry=fixtures)\n ","path":["Modules","Fixtures"],"tags":[]},{"location":"module/fixtures/#contexts","level":2,"title":"Contexts","text":"Context is an enum with predefined values:
Context Description Context.BASE Core data required in all environments Context.TESTING Data only loaded during tests Context.PRODUCTION Data only loaded in production A fixture with no contexts defined takes Context.BASE by default.
","path":["Modules","Fixtures"],"tags":[]},{"location":"module/fixtures/#load-strategies","level":2,"title":"Load strategies","text":"LoadStrategy controls how the fixture loader handles rows that already exist:
Strategy Description LoadStrategy.INSERT Insert only, fail on duplicates LoadStrategy.UPSERT Insert or update on conflict LoadStrategy.SKIP Skip rows that already exist","path":["Modules","Fixtures"],"tags":[]},{"location":"module/fixtures/#merging-registries","level":2,"title":"Merging registries","text":"Split fixtures definitions across modules and merge them:
from myapp.fixtures.dev import dev_fixtures\nfrom myapp.fixtures.prod import prod_fixtures\n\nfixtures = fixturesRegistry()\nfixtures.include_registry(registry=dev_fixtures)\nfixtures.include_registry(registry=prod_fixtures)\n\n## Pytest integration\n\nUse [`register_fixtures`](../reference/pytest.md#fastapi_toolsets.pytest.plugin.register_fixtures) to expose each fixture in your registry as an injectable pytest fixture named `fixture_{name}` by default:\n\n```python\n# conftest.py\nimport pytest\nfrom fastapi_toolsets.pytest import create_db_session, register_fixtures\nfrom app.fixtures import registry\nfrom app.models import Base\n\nDATABASE_URL = \"postgresql+asyncpg://user:pass@localhost/test_db\"\n\n@pytest.fixture\nasync def db_session():\n async with create_db_session(database_url=DATABASE_URL, base=Base, cleanup=True) as session:\n yield session\n\nregister_fixtures(registry=registry, namespace=globals())\n # test_users.py\nasync def test_user_can_login(fixture_users: list[User], fixture_roles: list[Role]):\n ...\n The load order is resolved automatically from the depends_on declarations in your registry. Each generated fixture receives db_session as a dependency and returns the list of loaded model instances.
","path":["Modules","Fixtures"],"tags":[]},{"location":"module/fixtures/#cli-integration","level":2,"title":"CLI integration","text":"Fixtures can be triggered from the CLI. See the CLI module for setup instructions.
API Reference
","path":["Modules","Fixtures"],"tags":[]},{"location":"module/logger/","level":1,"title":"Logger","text":"Lightweight logging utilities with consistent formatting and uvicorn integration.
","path":["Modules","Logger"],"tags":[]},{"location":"module/logger/#overview","level":2,"title":"Overview","text":"The logger module provides two helpers: one to configure the root logger (and uvicorn loggers) at startup, and one to retrieve a named logger anywhere in your codebase.
","path":["Modules","Logger"],"tags":[]},{"location":"module/logger/#setup","level":2,"title":"Setup","text":"Call configure_logging once at application startup:
from fastapi_toolsets.logger import configure_logging\n\nconfigure_logging(level=\"INFO\")\n This sets up a stdout handler with a consistent format and also configures uvicorn's access and error loggers so all log output shares the same style.
","path":["Modules","Logger"],"tags":[]},{"location":"module/logger/#getting-a-logger","level":2,"title":"Getting a logger","text":"from fastapi_toolsets.logger import get_logger\n\nlogger = get_logger(name=__name__)\nlogger.info(\"User created\")\n When called without arguments, get_logger auto-detects the caller's module name via frame inspection:
# Equivalent to get_logger(name=__name__)\nlogger = get_logger()\n API Reference
","path":["Modules","Logger"],"tags":[]},{"location":"module/metrics/","level":1,"title":"Metrics","text":"Prometheus metrics integration with a decorator-based registry and multi-process support.
","path":["Modules","Metrics"],"tags":[]},{"location":"module/metrics/#installation","level":2,"title":"Installation","text":"uvpip uv add \"fastapi-toolsets[metrics]\"\n pip install \"fastapi-toolsets[metrics]\"\n ","path":["Modules","Metrics"],"tags":[]},{"location":"module/metrics/#overview","level":2,"title":"Overview","text":"The metrics module provides a MetricsRegistry to declare Prometheus metrics with decorators, and an init_metrics function to mount a /metrics endpoint on your FastAPI app.
","path":["Modules","Metrics"],"tags":[]},{"location":"module/metrics/#setup","level":2,"title":"Setup","text":"from fastapi import FastAPI\nfrom fastapi_toolsets.metrics import MetricsRegistry, init_metrics\n\napp = FastAPI()\nmetrics = MetricsRegistry()\n\ninit_metrics(app=app, registry=metrics)\n This mounts the /metrics endpoint that Prometheus can scrape.
","path":["Modules","Metrics"],"tags":[]},{"location":"module/metrics/#declaring-metrics","level":2,"title":"Declaring metrics","text":"","path":["Modules","Metrics"],"tags":[]},{"location":"module/metrics/#providers","level":3,"title":"Providers","text":"Providers are called once at startup by init_metrics. The return value (the Prometheus metric object) is stored in the registry and can be retrieved later with registry.get(name).
Use providers when you want deferred initialization: the Prometheus metric is not registered with the global CollectorRegistry until init_metrics runs, not at import time. This is particularly useful for testing — importing the module in a test suite without calling init_metrics leaves no metrics registered, avoiding cross-test pollution.
It is also useful when metrics are defined across multiple modules and merged with include_registry: any code that needs a metric can call metrics.get() on the shared registry instead of importing the metric directly from its origin module.
If neither of these applies to you, declaring metrics at module level (e.g. HTTP_REQUESTS = Counter(...)) is simpler and equally valid.
from prometheus_client import Counter, Histogram\n\n@metrics.register\ndef http_requests():\n return Counter(\"http_requests_total\", \"Total HTTP requests\", [\"method\", \"status\"])\n\n@metrics.register\ndef request_duration():\n return Histogram(\"request_duration_seconds\", \"Request duration\")\n To use a provider's metric elsewhere (e.g. in a middleware), call metrics.get() inside the handler — not at module level, as providers are only initialized when init_metrics runs:
async def metrics_middleware(request: Request, call_next):\n response = await call_next(request)\n metrics.get(\"http_requests\").labels(\n method=request.method, status=response.status_code\n ).inc()\n return response\n ","path":["Modules","Metrics"],"tags":[]},{"location":"module/metrics/#collectors","level":3,"title":"Collectors","text":"Collectors are called on every scrape. Use them for metrics that reflect current state (e.g. gauges).
Declare the metric at module level
Do not instantiate the Prometheus metric inside the collector function. Doing so recreates it on every scrape, raising ValueError: Duplicated timeseries in CollectorRegistry. Declare it once at module level instead:
from prometheus_client import Gauge\n\n_queue_depth = Gauge(\"queue_depth\", \"Current queue depth\")\n\n@metrics.register(collect=True)\ndef collect_queue_depth():\n _queue_depth.set(get_current_queue_depth())\n ","path":["Modules","Metrics"],"tags":[]},{"location":"module/metrics/#merging-registries","level":2,"title":"Merging registries","text":"Split metrics definitions across modules and merge them:
from myapp.metrics.http import http_metrics\nfrom myapp.metrics.db import db_metrics\n\nmetrics = MetricsRegistry()\nmetrics.include_registry(registry=http_metrics)\nmetrics.include_registry(registry=db_metrics)\n ","path":["Modules","Metrics"],"tags":[]},{"location":"module/metrics/#multi-process-mode","level":2,"title":"Multi-process mode","text":"Multi-process support is enabled automatically when the PROMETHEUS_MULTIPROC_DIR environment variable is set. No code changes are required.
Environment variable name
The correct variable is PROMETHEUS_MULTIPROC_DIR (not PROMETHEUS_MULTIPROCESS_DIR).
API Reference
","path":["Modules","Metrics"],"tags":[]},{"location":"module/models/","level":1,"title":"Models","text":"Added in v2.0
Reusable SQLAlchemy 2.0 mixins for common column patterns, designed to be composed freely on any DeclarativeBase model.
","path":["Modules","Models"],"tags":[]},{"location":"module/models/#overview","level":2,"title":"Overview","text":"The models module provides mixins that each add a single, well-defined column behaviour. They work with standard SQLAlchemy 2.0 declarative syntax and are fully compatible with AsyncSession.
from fastapi_toolsets.models import UUIDMixin, TimestampMixin\n\nclass Article(Base, UUIDMixin, TimestampMixin):\n __tablename__ = \"articles\"\n\n title: Mapped[str]\n content: Mapped[str]\n All timestamp columns are timezone-aware (TIMESTAMPTZ). All defaults are server-side, so they are also applied when inserting rows via raw SQL outside the ORM.
","path":["Modules","Models"],"tags":[]},{"location":"module/models/#mixins","level":2,"title":"Mixins","text":"","path":["Modules","Models"],"tags":[]},{"location":"module/models/#uuidmixin","level":3,"title":"UUIDMixin","text":"Adds a id: UUID primary key generated server-side by PostgreSQL using gen_random_uuid() (requires PostgreSQL 13+). The value is retrieved via RETURNING after insert, so it is available on the Python object immediately after flush().
from fastapi_toolsets.models import UUIDMixin\n\nclass User(Base, UUIDMixin):\n __tablename__ = \"users\"\n\n username: Mapped[str]\n\n# id is None before flush\nuser = User(username=\"alice\")\nawait session.flush()\nprint(user.id) # UUID('...')\n ","path":["Modules","Models"],"tags":[]},{"location":"module/models/#createdatmixin","level":3,"title":"CreatedAtMixin","text":"Adds a created_at: datetime column set to NOW() on insert. The column has no onupdate hook — it is intentionally immutable after the row is created.
from fastapi_toolsets.models import UUIDMixin, CreatedAtMixin\n\nclass Order(Base, UUIDMixin, CreatedAtMixin):\n __tablename__ = \"orders\"\n\n total: Mapped[float]\n ","path":["Modules","Models"],"tags":[]},{"location":"module/models/#updatedatmixin","level":3,"title":"UpdatedAtMixin","text":"Adds an updated_at: datetime column set to NOW() on insert and automatically updated to NOW() on every ORM-level update (via SQLAlchemy's onupdate hook).
from fastapi_toolsets.models import UUIDMixin, UpdatedAtMixin\n\nclass Post(Base, UUIDMixin, UpdatedAtMixin):\n __tablename__ = \"posts\"\n\n title: Mapped[str]\n\npost = Post(title=\"Hello\")\nawait session.flush()\nawait session.refresh(post)\n\npost.title = \"Hello World\"\nawait session.flush()\nawait session.refresh(post)\nprint(post.updated_at)\n Note
updated_at is updated by SQLAlchemy at ORM flush time. If you update rows via raw SQL (e.g. UPDATE posts SET ...), the column will not be updated automatically — use a database trigger if you need that guarantee.
","path":["Modules","Models"],"tags":[]},{"location":"module/models/#timestampmixin","level":3,"title":"TimestampMixin","text":"Convenience mixin that combines CreatedAtMixin and UpdatedAtMixin. Equivalent to inheriting both.
from fastapi_toolsets.models import UUIDMixin, TimestampMixin\n\nclass Article(Base, UUIDMixin, TimestampMixin):\n __tablename__ = \"articles\"\n\n title: Mapped[str]\n ","path":["Modules","Models"],"tags":[]},{"location":"module/models/#composing-mixins","level":2,"title":"Composing mixins","text":"All mixins can be combined in any order. The only constraint is that exactly one primary key must be defined — either via UUIDMixin or directly on the model.
from fastapi_toolsets.models import UUIDMixin, TimestampMixin\n\nclass Event(Base, UUIDMixin, TimestampMixin):\n __tablename__ = \"events\"\n name: Mapped[str]\n\nclass Counter(Base, UpdatedAtMixin):\n __tablename__ = \"counters\"\n id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)\n value: Mapped[int]\n API Reference
","path":["Modules","Models"],"tags":[]},{"location":"module/pytest/","level":1,"title":"Pytest","text":"Testing helpers for FastAPI applications with async client, database sessions, and parallel worker support.
","path":["Modules","Pytest"],"tags":[]},{"location":"module/pytest/#installation","level":2,"title":"Installation","text":"uvpip uv add \"fastapi-toolsets[pytest]\"\n pip install \"fastapi-toolsets[pytest]\"\n ","path":["Modules","Pytest"],"tags":[]},{"location":"module/pytest/#overview","level":2,"title":"Overview","text":"The pytest module provides utilities for setting up async test clients, managing test database sessions, and supporting parallel test execution with pytest-xdist.
","path":["Modules","Pytest"],"tags":[]},{"location":"module/pytest/#creating-an-async-client","level":2,"title":"Creating an async client","text":"Use create_async_client to get an httpx.AsyncClient configured for your FastAPI app:
from fastapi_toolsets.pytest import create_async_client\n\n@pytest.fixture\nasync def http_client(db_session):\n async def _override_get_db():\n yield db_session\n\n async with create_async_client(\n app=app,\n base_url=\"http://127.0.0.1/api/v1\",\n dependency_overrides={get_db: _override_get_db},\n ) as c:\n yield c\n ","path":["Modules","Pytest"],"tags":[]},{"location":"module/pytest/#database-sessions-in-tests","level":2,"title":"Database sessions in tests","text":"Use create_db_session to create an isolated AsyncSession for a test, combined with create_worker_database to set up a per-worker database:
from fastapi_toolsets.pytest import create_worker_database, create_db_session\n\n@pytest.fixture(scope=\"session\")\nasync def worker_db_url():\n async with create_worker_database(\n database_url=str(settings.SQLALCHEMY_DATABASE_URI)\n ) as url:\n yield url\n\n\n@pytest.fixture\nasync def db_session(worker_db_url):\n async with create_db_session(\n database_url=worker_db_url, base=Base, cleanup=True\n ) as session:\n yield session\n Info
In this example, the database is reset between each test using the argument cleanup=True.
Use worker_database_url to derive the per-worker URL manually if needed:
from fastapi_toolsets.pytest import worker_database_url\n\nurl = worker_database_url(\"postgresql+asyncpg://user:pass@localhost/test_db\", default_test_db=\"test\")\n# e.g. \"postgresql+asyncpg://user:pass@localhost/test_db_gw0\" under xdist\n ","path":["Modules","Pytest"],"tags":[]},{"location":"module/pytest/#parallel-testing-with-pytest-xdist","level":2,"title":"Parallel testing with pytest-xdist","text":"The examples above are already compatible with parallel test execution with pytest-xdist.
","path":["Modules","Pytest"],"tags":[]},{"location":"module/pytest/#cleaning-up-tables","level":2,"title":"Cleaning up tables","text":"Warning
Since V2.1.0 cleanup_tables now live in fastapi_toolsets.db. For backward compatibility the function is still available in fastapi_toolsets.pytest, but this will be remove in V3.0.0.
If you want to manually clean up a database you can use cleanup_tables, this will truncate all tables between tests for fast isolation:
from fastapi_toolsets.db import cleanup_tables\n\n@pytest.fixture(autouse=True)\nasync def clean(db_session):\n yield\n await cleanup_tables(session=db_session, base=Base)\n API Reference
","path":["Modules","Pytest"],"tags":[]},{"location":"module/schemas/","level":1,"title":"Schemas","text":"Standardized Pydantic response models for consistent API responses across your FastAPI application.
","path":["Modules","Schemas"],"tags":[]},{"location":"module/schemas/#overview","level":2,"title":"Overview","text":"The schemas module provides generic response wrappers that enforce a uniform response structure. All models use from_attributes=True for ORM compatibility and validate_assignment=True for runtime type safety.
","path":["Modules","Schemas"],"tags":[]},{"location":"module/schemas/#response-models","level":2,"title":"Response models","text":"","path":["Modules","Schemas"],"tags":[]},{"location":"module/schemas/#responset","level":3,"title":"Response[T]","text":"The most common wrapper for a single resource response.
from fastapi_toolsets.schemas import Response\n\n@router.get(\"/users/{id}\")\nasync def get_user(user: User = UserDep) -> Response[UserSchema]:\n return Response(data=user, message=\"User retrieved\")\n ","path":["Modules","Schemas"],"tags":[]},{"location":"module/schemas/#paginatedresponset","level":3,"title":"PaginatedResponse[T]","text":"Wraps a list of items with pagination metadata and optional facet values. The pagination field accepts either OffsetPagination or CursorPagination depending on the strategy used.
","path":["Modules","Schemas"],"tags":[]},{"location":"module/schemas/#offsetpagination","level":4,"title":"OffsetPagination","text":"Page-number based. Requires total_count so clients can compute the total number of pages.
from fastapi_toolsets.schemas import PaginatedResponse, OffsetPagination\n\n@router.get(\"/users\")\nasync def list_users() -> PaginatedResponse[UserSchema]:\n return PaginatedResponse(\n data=users,\n pagination=OffsetPagination(\n total_count=100,\n items_per_page=10,\n page=1,\n has_more=True,\n ),\n )\n ","path":["Modules","Schemas"],"tags":[]},{"location":"module/schemas/#cursorpagination","level":4,"title":"CursorPagination","text":"Cursor based. Efficient for large or frequently updated datasets where offset pagination is impractical. Provides opaque next_cursor / prev_cursor tokens; no total count is exposed.
from fastapi_toolsets.schemas import PaginatedResponse, CursorPagination\n\n@router.get(\"/events\")\nasync def list_events() -> PaginatedResponse[EventSchema]:\n return PaginatedResponse(\n data=events,\n pagination=CursorPagination(\n next_cursor=\"eyJpZCI6IDQyfQ==\",\n prev_cursor=None,\n items_per_page=20,\n has_more=True,\n ),\n )\n The optional filter_attributes field is populated when facet_fields are configured on the CRUD class (see Filter attributes). It is None by default and can be hidden from API responses with response_model_exclude_none=True.
","path":["Modules","Schemas"],"tags":[]},{"location":"module/schemas/#errorresponse","level":3,"title":"ErrorResponse","text":"Returned automatically by the exceptions handler.
API Reference
","path":["Modules","Schemas"],"tags":[]},{"location":"reference/cli/","level":1,"title":"cli","text":"Here's the reference for the CLI configuration helpers used to load settings from pyproject.toml.
You can import them directly from fastapi_toolsets.cli.config:
from fastapi_toolsets.cli.config import (\n import_from_string,\n get_config_value,\n get_fixtures_registry,\n get_db_context,\n get_custom_cli,\n)\n ","path":["Reference","cli"],"tags":[]},{"location":"reference/cli/#fastapi_toolsets.cli.config.import_from_string","level":2,"title":"fastapi_toolsets.cli.config.import_from_string(import_path)","text":"Import an object from a dotted string path.
Parameters:
Name Type Description Default import_path str Import path in \"module.submodule:attribute\" format
required Returns:
Type Description Any The imported attribute
Raises:
Type Description BadParameter If the import path is invalid or import fails
","path":["Reference","cli"],"tags":[]},{"location":"reference/cli/#fastapi_toolsets.cli.config.get_config_value","level":2,"title":"fastapi_toolsets.cli.config.get_config_value(key, required=False)","text":"get_config_value(key: str, required: Literal[True]) -> Any\nget_config_value(\n key: str, required: bool = False\n) -> Any | None\n Get a configuration value from pyproject.toml.
Parameters:
Name Type Description Default key str The configuration key in [tool.fastapi-toolsets].
required required bool If True, raises an error when the key is missing.
False Returns:
Type Description Any | None The configuration value, or None if not found and not required.
Raises:
Type Description BadParameter If required=True and the key is missing.
","path":["Reference","cli"],"tags":[]},{"location":"reference/cli/#fastapi_toolsets.cli.config.get_fixtures_registry","level":2,"title":"fastapi_toolsets.cli.config.get_fixtures_registry()","text":"Import and return the fixtures registry from config.
","path":["Reference","cli"],"tags":[]},{"location":"reference/cli/#fastapi_toolsets.cli.config.get_db_context","level":2,"title":"fastapi_toolsets.cli.config.get_db_context()","text":"Import and return the db_context function from config.
","path":["Reference","cli"],"tags":[]},{"location":"reference/cli/#fastapi_toolsets.cli.config.get_custom_cli","level":2,"title":"fastapi_toolsets.cli.config.get_custom_cli()","text":"Import and return the custom CLI Typer instance from config.
","path":["Reference","cli"],"tags":[]},{"location":"reference/cli/#fastapi_toolsets.cli.utils.async_command","level":2,"title":"fastapi_toolsets.cli.utils.async_command(func)","text":"Decorator to run an async function as a sync CLI command.
Example @fixture_cli.command(\"load\")\n@async_command\nasync def load(ctx: typer.Context) -> None:\n async with get_db_context() as session:\n await load_fixtures(session, registry)\n ","path":["Reference","cli"],"tags":[]},{"location":"reference/crud/","level":1,"title":"crud","text":"Here's the reference for the CRUD classes, factory, and search utilities.
You can import the main symbols from fastapi_toolsets.crud:
from fastapi_toolsets.crud import CrudFactory, AsyncCrud\nfrom fastapi_toolsets.crud.search import SearchConfig, get_searchable_fields, build_search_filters\n ","path":["Reference","crud"],"tags":[]},{"location":"reference/crud/#fastapi_toolsets.crud.factory.AsyncCrud","level":2,"title":"fastapi_toolsets.crud.factory.AsyncCrud","text":" Bases: Generic[ModelType]
Generic async CRUD operations for SQLAlchemy models.
Subclass this and set the model class variable, or use CrudFactory.
","path":["Reference","crud"],"tags":[]},{"location":"reference/crud/#fastapi_toolsets.crud.factory.AsyncCrud.count","level":3,"title":"count(session, filters=None, *, joins=None, outer_join=False) async classmethod","text":"Count records matching the filters.
Parameters:
Name Type Description Default session AsyncSession DB async session
required filters list[Any] | None List of SQLAlchemy filter conditions
None joins JoinType | None List of (model, condition) tuples for joining related tables
None outer_join bool Use LEFT OUTER JOIN instead of INNER JOIN
False Returns:
Type Description int Number of matching records
","path":["Reference","crud"],"tags":[]},{"location":"reference/crud/#fastapi_toolsets.crud.factory.AsyncCrud.create","level":3,"title":"create(session, obj, *, schema=None) async classmethod","text":"create(\n session: AsyncSession,\n obj: BaseModel,\n *,\n schema: type[SchemaType],\n) -> Response[SchemaType]\ncreate(\n session: AsyncSession,\n obj: BaseModel,\n *,\n schema: None = ...,\n) -> ModelType\n Create a new record in the database.
Parameters:
Name Type Description Default session AsyncSession DB async session
required obj BaseModel Pydantic model with data to create
required schema type[BaseModel] | None Pydantic schema to serialize the result into. When provided, the result is automatically wrapped in a Response[schema].
None Returns:
Type Description ModelType | Response[Any] Created model instance, or Response[schema] when schema is given.
","path":["Reference","crud"],"tags":[]},{"location":"reference/crud/#fastapi_toolsets.crud.factory.AsyncCrud.cursor_paginate","level":3,"title":"cursor_paginate(session, *, cursor=None, filters=None, joins=None, outer_join=False, load_options=None, order_by=None, items_per_page=20, search=None, search_fields=None, facet_fields=None, filter_by=None, schema) async classmethod","text":"Get paginated results using cursor-based pagination.
Parameters:
Name Type Description Default session AsyncSession DB async session.
required cursor str | None Cursor string from a previous CursorPagination. Omit (or pass None) to start from the beginning.
None filters list[Any] | None List of SQLAlchemy filter conditions.
None joins JoinType | None List of (model, condition) tuples for joining related tables.
None outer_join bool Use LEFT OUTER JOIN instead of INNER JOIN.
False load_options Sequence[ExecutableOption] | None SQLAlchemy loader options. Falls back to default_load_options when not provided.
None order_by OrderByClause | None Additional ordering applied after the cursor column.
None items_per_page int Number of items per page (default 20).
20 search str | SearchConfig | None Search query string or SearchConfig object.
None search_fields Sequence[SearchFieldType] | None Fields to search in (overrides class default).
None facet_fields Sequence[FacetFieldType] | None Columns to compute distinct values for (overrides class default).
None filter_by dict[str, Any] | BaseModel | None Dict of {column_key: value} to filter by declared facet fields. Keys must match the column.key of a facet field. Scalar → equality, list → IN clause. Raises InvalidFacetFilterError for unknown keys.
None schema type[BaseModel] Optional Pydantic schema to serialize each item into.
required Returns:
Type Description PaginatedResponse[Any] PaginatedResponse with CursorPagination metadata
","path":["Reference","crud"],"tags":[]},{"location":"reference/crud/#fastapi_toolsets.crud.factory.AsyncCrud.delete","level":3,"title":"delete(session, filters, *, return_response=False) async classmethod","text":"delete(\n session: AsyncSession,\n filters: list[Any],\n *,\n return_response: Literal[True],\n) -> Response[None]\ndelete(\n session: AsyncSession,\n filters: list[Any],\n *,\n return_response: Literal[False] = ...,\n) -> None\n Delete records from the database.
Parameters:
Name Type Description Default session AsyncSession DB async session
required filters list[Any] List of SQLAlchemy filter conditions
required return_response bool When True, returns Response[None] instead of None. Useful for API endpoints that expect a consistent response envelope.
False Returns:
Type Description None | Response[None] None, or Response[None] when return_response=True.
","path":["Reference","crud"],"tags":[]},{"location":"reference/crud/#fastapi_toolsets.crud.factory.AsyncCrud.exists","level":3,"title":"exists(session, filters, *, joins=None, outer_join=False) async classmethod","text":"Check if a record exists.
Parameters:
Name Type Description Default session AsyncSession DB async session
required filters list[Any] List of SQLAlchemy filter conditions
required joins JoinType | None List of (model, condition) tuples for joining related tables
None outer_join bool Use LEFT OUTER JOIN instead of INNER JOIN
False Returns:
Type Description bool True if at least one record matches
","path":["Reference","crud"],"tags":[]},{"location":"reference/crud/#fastapi_toolsets.crud.factory.AsyncCrud.filter_params","level":3,"title":"filter_params(*, facet_fields=None) classmethod","text":"Return a FastAPI dependency that collects facet filter values from query parameters. Args: facet_fields: Override the facet fields for this dependency. Falls back to the class-level facet_fields if not provided.
Returns:
Type Description Callable[..., Awaitable[dict[str, list[str]]]] An async dependency function named {Model}FilterParams that resolves to a
Callable[..., Awaitable[dict[str, list[str]]]] dict[str, list[str]] containing only the keys that were supplied in the
Callable[..., Awaitable[dict[str, list[str]]]] request (absent/None parameters are excluded).
Raises:
Type Description ValueError If no facet fields are configured on this CRUD class and none are provided via facet_fields.
","path":["Reference","crud"],"tags":[]},{"location":"reference/crud/#fastapi_toolsets.crud.factory.AsyncCrud.first","level":3,"title":"first(session, filters=None, *, joins=None, outer_join=False, with_for_update=False, load_options=None, schema=None) async classmethod","text":"first(\n session: AsyncSession,\n filters: list[Any] | None = None,\n *,\n joins: JoinType | None = None,\n outer_join: bool = False,\n with_for_update: bool = False,\n load_options: Sequence[ExecutableOption] | None = None,\n schema: type[SchemaType],\n) -> Response[SchemaType] | None\nfirst(\n session: AsyncSession,\n filters: list[Any] | None = None,\n *,\n joins: JoinType | None = None,\n outer_join: bool = False,\n with_for_update: bool = False,\n load_options: Sequence[ExecutableOption] | None = None,\n schema: None = ...,\n) -> ModelType | None\n Get the first matching record, or None.
Parameters:
Name Type Description Default session AsyncSession DB async session
required filters list[Any] | None List of SQLAlchemy filter conditions
None joins JoinType | None List of (model, condition) tuples for joining related tables
None outer_join bool Use LEFT OUTER JOIN instead of INNER JOIN
False with_for_update bool Lock the row for update
False load_options Sequence[ExecutableOption] | None SQLAlchemy loader options (e.g., selectinload)
None schema type[BaseModel] | None Pydantic schema to serialize the result into. When provided, the result is automatically wrapped in a Response[schema].
None Returns:
Type Description ModelType | Response[Any] | None Model instance, Response[schema] when schema is given,
ModelType | Response[Any] | None or None when no record matches.
","path":["Reference","crud"],"tags":[]},{"location":"reference/crud/#fastapi_toolsets.crud.factory.AsyncCrud.get","level":3,"title":"get(session, filters, *, joins=None, outer_join=False, with_for_update=False, load_options=None, schema=None) async classmethod","text":"get(\n session: AsyncSession,\n filters: list[Any],\n *,\n joins: JoinType | None = None,\n outer_join: bool = False,\n with_for_update: bool = False,\n load_options: Sequence[ExecutableOption] | None = None,\n schema: type[SchemaType],\n) -> Response[SchemaType]\nget(\n session: AsyncSession,\n filters: list[Any],\n *,\n joins: JoinType | None = None,\n outer_join: bool = False,\n with_for_update: bool = False,\n load_options: Sequence[ExecutableOption] | None = None,\n schema: None = ...,\n) -> ModelType\n Get exactly one record. Raises NotFoundError if not found.
Parameters:
Name Type Description Default session AsyncSession DB async session
required filters list[Any] List of SQLAlchemy filter conditions
required joins JoinType | None List of (model, condition) tuples for joining related tables
None outer_join bool Use LEFT OUTER JOIN instead of INNER JOIN
False with_for_update bool Lock the row for update
False load_options Sequence[ExecutableOption] | None SQLAlchemy loader options (e.g., selectinload)
None schema type[BaseModel] | None Pydantic schema to serialize the result into. When provided, the result is automatically wrapped in a Response[schema].
None Returns:
Type Description ModelType | Response[Any] Model instance, or Response[schema] when schema is given.
Raises:
Type Description NotFoundError If no record found
MultipleResultsFound If more than one record found
","path":["Reference","crud"],"tags":[]},{"location":"reference/crud/#fastapi_toolsets.crud.factory.AsyncCrud.get_multi","level":3,"title":"get_multi(session, *, filters=None, joins=None, outer_join=False, load_options=None, order_by=None, limit=None, offset=None) async classmethod","text":"Get multiple records from the database.
Parameters:
Name Type Description Default session AsyncSession DB async session
required filters list[Any] | None List of SQLAlchemy filter conditions
None joins JoinType | None List of (model, condition) tuples for joining related tables
None outer_join bool Use LEFT OUTER JOIN instead of INNER JOIN
False load_options Sequence[ExecutableOption] | None SQLAlchemy loader options
None order_by OrderByClause | None Column or list of columns to order by
None limit int | None Max number of rows to return
None offset int | None Rows to skip
None Returns:
Type Description Sequence[ModelType] List of model instances
","path":["Reference","crud"],"tags":[]},{"location":"reference/crud/#fastapi_toolsets.crud.factory.AsyncCrud.get_or_none","level":3,"title":"get_or_none(session, filters, *, joins=None, outer_join=False, with_for_update=False, load_options=None, schema=None) async classmethod","text":"get_or_none(\n session: AsyncSession,\n filters: list[Any],\n *,\n joins: JoinType | None = None,\n outer_join: bool = False,\n with_for_update: bool = False,\n load_options: Sequence[ExecutableOption] | None = None,\n schema: type[SchemaType],\n) -> Response[SchemaType] | None\nget_or_none(\n session: AsyncSession,\n filters: list[Any],\n *,\n joins: JoinType | None = None,\n outer_join: bool = False,\n with_for_update: bool = False,\n load_options: Sequence[ExecutableOption] | None = None,\n schema: None = ...,\n) -> ModelType | None\n Get exactly one record, or None if not found.
Like :meth:get but returns None instead of raising :class:~fastapi_toolsets.exceptions.NotFoundError when no record matches the filters.
Parameters:
Name Type Description Default session AsyncSession DB async session
required filters list[Any] List of SQLAlchemy filter conditions
required joins JoinType | None List of (model, condition) tuples for joining related tables
None outer_join bool Use LEFT OUTER JOIN instead of INNER JOIN
False with_for_update bool Lock the row for update
False load_options Sequence[ExecutableOption] | None SQLAlchemy loader options (e.g., selectinload)
None schema type[BaseModel] | None Pydantic schema to serialize the result into. When provided, the result is automatically wrapped in a Response[schema].
None Returns:
Type Description ModelType | Response[Any] | None Model instance, Response[schema] when schema is given,
ModelType | Response[Any] | None or None when no record matches.
Raises:
Type Description MultipleResultsFound If more than one record found
","path":["Reference","crud"],"tags":[]},{"location":"reference/crud/#fastapi_toolsets.crud.factory.AsyncCrud.offset_paginate","level":3,"title":"offset_paginate(session, *, filters=None, joins=None, outer_join=False, load_options=None, order_by=None, page=1, items_per_page=20, search=None, search_fields=None, facet_fields=None, filter_by=None, schema) async classmethod","text":"Get paginated results using offset-based pagination.
Parameters:
Name Type Description Default session AsyncSession DB async session
required filters list[Any] | None List of SQLAlchemy filter conditions
None joins JoinType | None List of (model, condition) tuples for joining related tables
None outer_join bool Use LEFT OUTER JOIN instead of INNER JOIN
False load_options Sequence[ExecutableOption] | None SQLAlchemy loader options
None order_by OrderByClause | None Column or list of columns to order by
None page int Page number (1-indexed)
1 items_per_page int Number of items per page
20 search str | SearchConfig | None Search query string or SearchConfig object
None search_fields Sequence[SearchFieldType] | None Fields to search in (overrides class default)
None facet_fields Sequence[FacetFieldType] | None Columns to compute distinct values for (overrides class default)
None filter_by dict[str, Any] | BaseModel | None Dict of {column_key: value} to filter by declared facet fields. Keys must match the column.key of a facet field. Scalar → equality, list → IN clause. Raises InvalidFacetFilterError for unknown keys.
None schema type[BaseModel] Pydantic schema to serialize each item into.
required Returns:
Type Description PaginatedResponse[Any] PaginatedResponse with OffsetPagination metadata
","path":["Reference","crud"],"tags":[]},{"location":"reference/crud/#fastapi_toolsets.crud.factory.AsyncCrud.order_params","level":3,"title":"order_params(*, order_fields=None, default_field=None, default_order='asc') classmethod","text":"Return a FastAPI dependency that resolves order query params into an order_by clause.
Parameters:
Name Type Description Default order_fields Sequence[QueryableAttribute[Any]] | None Override the allowed order fields. Falls back to the class-level order_fields if not provided.
None default_field QueryableAttribute[Any] | None Field to order by when order_by query param is absent. If None and no order_by is provided, no ordering is applied.
None default_order Literal['asc', 'desc'] Default order direction when order is absent (\"asc\" or \"desc\").
'asc' Returns:
Type Description Callable[..., Awaitable[OrderByClause | None]] An async dependency function named {Model}OrderParams that resolves to an
Callable[..., Awaitable[OrderByClause | None]] OrderByClause (or None). Pass it to Depends() in your route.
Raises:
Type Description ValueError If no order fields are configured on this CRUD class and none are provided via order_fields.
InvalidOrderFieldError When the request provides an unknown order_by value.
","path":["Reference","crud"],"tags":[]},{"location":"reference/crud/#fastapi_toolsets.crud.factory.AsyncCrud.update","level":3,"title":"update(session, obj, filters, *, exclude_unset=True, exclude_none=False, schema=None) async classmethod","text":"update(\n session: AsyncSession,\n obj: BaseModel,\n filters: list[Any],\n *,\n exclude_unset: bool = True,\n exclude_none: bool = False,\n schema: type[SchemaType],\n) -> Response[SchemaType]\nupdate(\n session: AsyncSession,\n obj: BaseModel,\n filters: list[Any],\n *,\n exclude_unset: bool = True,\n exclude_none: bool = False,\n schema: None = ...,\n) -> ModelType\n Update a record in the database.
Parameters:
Name Type Description Default session AsyncSession DB async session
required obj BaseModel Pydantic model with update data
required filters list[Any] List of SQLAlchemy filter conditions
required exclude_unset bool Exclude fields not explicitly set in the schema
True exclude_none bool Exclude fields with None value
False schema type[BaseModel] | None Pydantic schema to serialize the result into. When provided, the result is automatically wrapped in a Response[schema].
None Returns:
Type Description ModelType | Response[Any] Updated model instance, or Response[schema] when schema is given.
Raises:
Type Description NotFoundError If no record found
","path":["Reference","crud"],"tags":[]},{"location":"reference/crud/#fastapi_toolsets.crud.factory.AsyncCrud.upsert","level":3,"title":"upsert(session, obj, index_elements, *, set_=None, where=None) async classmethod","text":"Create or update a record (PostgreSQL only).
Uses INSERT ... ON CONFLICT for atomic upsert.
Parameters:
Name Type Description Default session AsyncSession DB async session
required obj BaseModel Pydantic model with data
required index_elements list[str] Columns for ON CONFLICT (unique constraint)
required set_ BaseModel | None Pydantic model for ON CONFLICT DO UPDATE SET
None where WhereHavingRole | None WHERE clause for ON CONFLICT DO UPDATE
None Returns:
Type Description ModelType | None Model instance
","path":["Reference","crud"],"tags":[]},{"location":"reference/crud/#fastapi_toolsets.crud.factory.CrudFactory","level":2,"title":"fastapi_toolsets.crud.factory.CrudFactory(model, *, searchable_fields=None, facet_fields=None, order_fields=None, m2m_fields=None, default_load_options=None, cursor_column=None)","text":"Create a CRUD class for a specific model.
Parameters:
Name Type Description Default model type[ModelType] SQLAlchemy model class
required searchable_fields Sequence[SearchFieldType] | None Optional list of searchable fields
None facet_fields Sequence[FacetFieldType] | None Optional list of columns to compute distinct values for in paginated responses. Supports direct columns (User.status) and relationship tuples ((User.role, Role.name)). Can be overridden per call.
None order_fields Sequence[QueryableAttribute[Any]] | None Optional list of model attributes that callers are allowed to order by via order_params(). Can be overridden per call.
None m2m_fields M2MFieldType | None Optional mapping for many-to-many relationships. Maps schema field names (containing lists of IDs) to SQLAlchemy relationship attributes.
None default_load_options Sequence[ExecutableOption] | None Default SQLAlchemy loader options applied to all read queries when no explicit load_options are passed. Use this instead of lazy=\"selectin\" on the model so that loading strategy is explicit and per-CRUD. Overridden entirely (not merged) when load_options is provided at call-site.
None cursor_column Any | None Required to call cursor_paginate. Must be monotonically ordered (e.g. integer PK, UUID v7, timestamp). See the cursor pagination docs for supported column types.
None Returns:
Type Description type[AsyncCrud[ModelType]] AsyncCrud subclass bound to the model
Example from fastapi_toolsets.crud import CrudFactory\nfrom myapp.models import User, Post\n\nUserCrud = CrudFactory(User)\nPostCrud = CrudFactory(Post)\n\n# With searchable fields:\nUserCrud = CrudFactory(\n User,\n searchable_fields=[User.username, User.email, (User.role, Role.name)]\n)\n\n# With many-to-many fields:\n# Schema has `tag_ids: list[UUID]`, model has `tags` relationship to Tag\nPostCrud = CrudFactory(\n Post,\n m2m_fields={\"tag_ids\": Post.tags},\n)\n\n# With facet fields for filter dropdowns / faceted search:\nUserCrud = CrudFactory(\n User,\n facet_fields=[User.status, User.country, (User.role, Role.name)],\n)\n\n# With a fixed cursor column for cursor_paginate:\nPostCrud = CrudFactory(\n Post,\n cursor_column=Post.created_at,\n)\n\n# With default load strategy (replaces lazy=\"selectin\" on the model):\nArticleCrud = CrudFactory(\n Article,\n default_load_options=[selectinload(Article.category), selectinload(Article.tags)],\n)\n\n# Override default_load_options for a specific call:\narticle = await ArticleCrud.get(\n session,\n [Article.id == 1],\n load_options=[selectinload(Article.category)], # tags won't load\n)\n\n# Usage\nuser = await UserCrud.get(session, [User.id == 1])\nposts = await PostCrud.get_multi(session, filters=[Post.user_id == user.id])\n\n# Create with M2M - tag_ids are automatically resolved\npost = await PostCrud.create(session, PostCreate(title=\"Hello\", tag_ids=[id1, id2]))\n\n# With search\nresult = await UserCrud.offset_paginate(session, search=\"john\")\n\n# With joins (inner join by default):\nusers = await UserCrud.get_multi(\n session,\n joins=[(Post, Post.user_id == User.id)],\n filters=[Post.published == True],\n)\n\n# With outer join:\nusers = await UserCrud.get_multi(\n session,\n joins=[(Post, Post.user_id == User.id)],\n outer_join=True,\n)\n ","path":["Reference","crud"],"tags":[]},{"location":"reference/crud/#fastapi_toolsets.crud.search.SearchConfig","level":2,"title":"fastapi_toolsets.crud.search.SearchConfig dataclass","text":"Advanced search configuration.
Attributes:
Name Type Description query str The search string
fields Sequence[SearchFieldType] | None Fields to search (columns or tuples for relationships)
case_sensitive bool Case-sensitive search (default: False)
match_mode Literal['any', 'all'] \"any\" (OR) or \"all\" (AND) to combine fields
","path":["Reference","crud"],"tags":[]},{"location":"reference/crud/#fastapi_toolsets.crud.search.get_searchable_fields","level":2,"title":"fastapi_toolsets.crud.search.get_searchable_fields(model, *, include_relationships=True, max_depth=1) cached","text":"Auto-detect String fields on a model and its relationships.
Parameters:
Name Type Description Default model type[DeclarativeBase] SQLAlchemy model class
required include_relationships bool Include fields from many-to-one/one-to-one relationships
True max_depth int Max depth for relationship traversal (default: 1)
1 Returns:
Type Description list[SearchFieldType] List of columns and tuples (relationship, column)
","path":["Reference","crud"],"tags":[]},{"location":"reference/crud/#fastapi_toolsets.crud.search.build_search_filters","level":2,"title":"fastapi_toolsets.crud.search.build_search_filters(model, search, search_fields=None, default_fields=None)","text":"Build SQLAlchemy filter conditions for search.
Parameters:
Name Type Description Default model type[DeclarativeBase] SQLAlchemy model class
required search str | SearchConfig Search string or SearchConfig
required search_fields Sequence[SearchFieldType] | None Fields specified per-call (takes priority)
None default_fields Sequence[SearchFieldType] | None Default fields (from ClassVar)
None Returns:
Type Description tuple[list[ColumnElement[bool]], list[InstrumentedAttribute[Any]]] Tuple of (filter_conditions, joins_needed)
Raises:
Type Description NoSearchableFieldsError If no searchable field has been configured
","path":["Reference","crud"],"tags":[]},{"location":"reference/db/","level":1,"title":"db","text":"Here's the reference for all database session utilities, transaction helpers, and locking functions.
You can import them directly from fastapi_toolsets.db:
from fastapi_toolsets.db import (\n LockMode,\n cleanup_tables,\n create_database,\n create_db_dependency,\n create_db_context,\n get_transaction,\n lock_tables,\n wait_for_row_change,\n)\n ","path":["Reference","db"],"tags":[]},{"location":"reference/db/#fastapi_toolsets.db.LockMode","level":2,"title":"fastapi_toolsets.db.LockMode","text":" Bases: str, Enum
PostgreSQL table lock modes.
See: https://www.postgresql.org/docs/current/explicit-locking.html
","path":["Reference","db"],"tags":[]},{"location":"reference/db/#fastapi_toolsets.db.create_db_dependency","level":2,"title":"fastapi_toolsets.db.create_db_dependency(session_maker)","text":"Create a FastAPI dependency for database sessions.
Creates a dependency function that yields a session and auto-commits if a transaction is active when the request completes.
Parameters:
Name Type Description Default session_maker async_sessionmaker[AsyncSession] Async session factory from create_session_factory()
required Returns:
Type Description Callable[[], AsyncGenerator[AsyncSession, None]] An async generator function usable with FastAPI's Depends()
Example from fastapi import Depends\nfrom sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker\nfrom fastapi_toolsets.db import create_db_dependency\n\nengine = create_async_engine(\"postgresql+asyncpg://...\")\nSessionLocal = async_sessionmaker(engine, expire_on_commit=False)\nget_db = create_db_dependency(SessionLocal)\n\n@app.get(\"/users\")\nasync def list_users(session: AsyncSession = Depends(get_db)):\n ...\n ","path":["Reference","db"],"tags":[]},{"location":"reference/db/#fastapi_toolsets.db.create_db_context","level":2,"title":"fastapi_toolsets.db.create_db_context(session_maker)","text":"Create a context manager for database sessions.
Creates a context manager for use outside of FastAPI request handlers, such as in background tasks, CLI commands, or tests.
Parameters:
Name Type Description Default session_maker async_sessionmaker[AsyncSession] Async session factory from create_session_factory()
required Returns:
Type Description Callable[[], AbstractAsyncContextManager[AsyncSession]] An async context manager function
Example from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker\nfrom fastapi_toolsets.db import create_db_context\n\nengine = create_async_engine(\"postgresql+asyncpg://...\")\nSessionLocal = async_sessionmaker(engine, expire_on_commit=False)\nget_db_context = create_db_context(SessionLocal)\n\nasync def background_task():\n async with get_db_context() as session:\n user = await UserCrud.get(session, [User.id == 1])\n ...\n ","path":["Reference","db"],"tags":[]},{"location":"reference/db/#fastapi_toolsets.db.get_transaction","level":2,"title":"fastapi_toolsets.db.get_transaction(session) async","text":"Get a transaction context, handling nested transactions.
If already in a transaction, creates a savepoint (nested transaction). Otherwise, starts a new transaction.
Parameters:
Name Type Description Default session AsyncSession AsyncSession instance
required Yields:
Type Description AsyncGenerator[AsyncSession, None] The session within the transaction context
Example async with get_transaction(session):\n session.add(model)\n # Auto-commits on exit, rolls back on exception\n ","path":["Reference","db"],"tags":[]},{"location":"reference/db/#fastapi_toolsets.db.lock_tables","level":2,"title":"fastapi_toolsets.db.lock_tables(session, tables, *, mode=LockMode.SHARE_UPDATE_EXCLUSIVE, timeout='5s') async","text":"Lock PostgreSQL tables for the duration of a transaction.
Acquires table-level locks that are held until the transaction ends. Useful for preventing concurrent modifications during critical operations.
Parameters:
Name Type Description Default session AsyncSession AsyncSession instance
required tables list[type[DeclarativeBase]] List of SQLAlchemy model classes to lock
required mode LockMode Lock mode (default: SHARE UPDATE EXCLUSIVE)
SHARE_UPDATE_EXCLUSIVE timeout str Lock timeout (default: \"5s\")
'5s' Yields:
Type Description AsyncGenerator[AsyncSession, None] The session with locked tables
Raises:
Type Description SQLAlchemyError If lock cannot be acquired within timeout
Example from fastapi_toolsets.db import lock_tables, LockMode\n\nasync with lock_tables(session, [User, Account]):\n # Tables are locked with SHARE UPDATE EXCLUSIVE mode\n user = await UserCrud.get(session, [User.id == 1])\n user.balance += 100\n\n# With custom lock mode\nasync with lock_tables(session, [Order], mode=LockMode.EXCLUSIVE):\n # Exclusive lock - no other transactions can access\n await process_order(session, order_id)\n ","path":["Reference","db"],"tags":[]},{"location":"reference/db/#fastapi_toolsets.db.wait_for_row_change","level":2,"title":"fastapi_toolsets.db.wait_for_row_change(session, model, pk_value, *, columns=None, interval=0.5, timeout=None) async","text":"Poll a database row until a change is detected.
Queries the row every interval seconds and returns the model instance once a change is detected in any column (or only the specified columns).
Parameters:
Name Type Description Default session AsyncSession AsyncSession instance
required model type[_M] SQLAlchemy model class
required pk_value Any Primary key value of the row to watch
required columns list[str] | None Optional list of column names to watch. If None, all columns are watched.
None interval float Polling interval in seconds (default: 0.5)
0.5 timeout float | None Maximum time to wait in seconds. None means wait forever.
None Returns:
Type Description _M The refreshed model instance with updated values
Raises:
Type Description NotFoundError If the row does not exist or is deleted during polling
TimeoutError If timeout expires before a change is detected
Example from fastapi_toolsets.db import wait_for_row_change\n\n# Wait for any column to change\nupdated = await wait_for_row_change(session, User, user_id)\n\n# Watch specific columns with a timeout\nupdated = await wait_for_row_change(\n session, User, user_id,\n columns=[\"status\", \"email\"],\n interval=1.0,\n timeout=30.0,\n)\n ","path":["Reference","db"],"tags":[]},{"location":"reference/db/#fastapi_toolsets.db.create_database","level":2,"title":"fastapi_toolsets.db.create_database(db_name, *, server_url) async","text":"Create a database.
Connects to server_url using AUTOCOMMIT isolation and issues a CREATE DATABASE statement for db_name.
Parameters:
Name Type Description Default db_name str Name of the database to create.
required server_url str URL used for server-level DDL (must point to an existing database on the same server).
required Example from fastapi_toolsets.db import create_database\n\nSERVER_URL = \"postgresql+asyncpg://postgres:postgres@localhost/postgres\"\nawait create_database(\"myapp_test\", server_url=SERVER_URL)\n ","path":["Reference","db"],"tags":[]},{"location":"reference/db/#fastapi_toolsets.db.cleanup_tables","level":2,"title":"fastapi_toolsets.db.cleanup_tables(session, base) async","text":"Truncate all tables for fast between-test cleanup.
Executes a single TRUNCATE … RESTART IDENTITY CASCADE statement across every table in base's metadata, which is significantly faster than dropping and re-creating tables between tests.
This is a no-op when the metadata contains no tables.
Parameters:
Name Type Description Default session AsyncSession An active async database session.
required base type[DeclarativeBase] SQLAlchemy DeclarativeBase class containing model metadata.
required Example @pytest.fixture\nasync def db_session(worker_db_url):\n async with create_db_session(worker_db_url, Base) as session:\n yield session\n await cleanup_tables(session, Base)\n ","path":["Reference","db"],"tags":[]},{"location":"reference/dependencies/","level":1,"title":"dependencies","text":"Here's the reference for the FastAPI dependency factory functions.
You can import them directly from fastapi_toolsets.dependencies:
from fastapi_toolsets.dependencies import PathDependency, BodyDependency\n ","path":["Reference","dependencies"],"tags":[]},{"location":"reference/dependencies/#fastapi_toolsets.dependencies.PathDependency","level":2,"title":"fastapi_toolsets.dependencies.PathDependency(model, field, *, session_dep, param_name=None)","text":"Create a dependency that fetches a DB object from a path parameter.
Parameters:
Name Type Description Default model type[ModelType] SQLAlchemy model class
required field Any Model field to filter by (e.g., User.id)
required session_dep SessionDependency Session dependency function (e.g., get_db)
required param_name str | None Path parameter name (defaults to model_field, e.g., user_id)
None Returns:
Type Description ModelType A Depends() instance that resolves to the model instance
Raises:
Type Description NotFoundError If no matching record is found
Example UserDep = PathDependency(User, User.id, session_dep=get_db)\n\n@router.get(\"/user/{id}\")\nasync def get(\n user: User = UserDep,\n): ...\n ","path":["Reference","dependencies"],"tags":[]},{"location":"reference/dependencies/#fastapi_toolsets.dependencies.BodyDependency","level":2,"title":"fastapi_toolsets.dependencies.BodyDependency(model, field, *, session_dep, body_field)","text":"Create a dependency that fetches a DB object from a body field.
Parameters:
Name Type Description Default model type[ModelType] SQLAlchemy model class
required field Any Model field to filter by (e.g., User.id)
required session_dep SessionDependency Session dependency function (e.g., get_db)
required body_field str Name of the field in the request body
required Returns:
Type Description ModelType A Depends() instance that resolves to the model instance
Raises:
Type Description NotFoundError If no matching record is found
Example UserDep = BodyDependency(\n User, User.ctfd_id, session_dep=get_db, body_field=\"user_id\"\n)\n\n@router.post(\"/assign\")\nasync def assign(\n user: User = UserDep,\n): ...\n ","path":["Reference","dependencies"],"tags":[]},{"location":"reference/exceptions/","level":1,"title":"exceptions","text":"Here's the reference for all exception classes and handler utilities.
You can import them directly from fastapi_toolsets.exceptions:
from fastapi_toolsets.exceptions import (\n ApiException,\n UnauthorizedError,\n ForbiddenError,\n NotFoundError,\n ConflictError,\n NoSearchableFieldsError,\n InvalidFacetFilterError,\n InvalidOrderFieldError,\n generate_error_responses,\n init_exceptions_handlers,\n)\n ","path":["Reference","exceptions"],"tags":[]},{"location":"reference/exceptions/#fastapi_toolsets.exceptions.exceptions.ApiException","level":2,"title":"fastapi_toolsets.exceptions.exceptions.ApiException","text":" Bases: Exception
Base exception for API errors with structured response.
","path":["Reference","exceptions"],"tags":[]},{"location":"reference/exceptions/#fastapi_toolsets.exceptions.exceptions.ApiException.__init__","level":3,"title":"__init__(detail=None, *, desc=None, data=None)","text":"Initialize the exception.
Parameters:
Name Type Description Default detail str | None Optional human-readable message
None desc str | None Optional per-instance override for the description field in the HTTP response body.
None data Any Optional per-instance override for the data field in the HTTP response body.
None","path":["Reference","exceptions"],"tags":[]},{"location":"reference/exceptions/#fastapi_toolsets.exceptions.exceptions.UnauthorizedError","level":2,"title":"fastapi_toolsets.exceptions.exceptions.UnauthorizedError","text":" Bases: ApiException
HTTP 401 - User is not authenticated.
","path":["Reference","exceptions"],"tags":[]},{"location":"reference/exceptions/#fastapi_toolsets.exceptions.exceptions.UnauthorizedError.__init__","level":3,"title":"__init__(detail=None, *, desc=None, data=None)","text":"Initialize the exception.
Parameters:
Name Type Description Default detail str | None Optional human-readable message
None desc str | None Optional per-instance override for the description field in the HTTP response body.
None data Any Optional per-instance override for the data field in the HTTP response body.
None","path":["Reference","exceptions"],"tags":[]},{"location":"reference/exceptions/#fastapi_toolsets.exceptions.exceptions.ForbiddenError","level":2,"title":"fastapi_toolsets.exceptions.exceptions.ForbiddenError","text":" Bases: ApiException
HTTP 403 - User lacks required permissions.
","path":["Reference","exceptions"],"tags":[]},{"location":"reference/exceptions/#fastapi_toolsets.exceptions.exceptions.ForbiddenError.__init__","level":3,"title":"__init__(detail=None, *, desc=None, data=None)","text":"Initialize the exception.
Parameters:
Name Type Description Default detail str | None Optional human-readable message
None desc str | None Optional per-instance override for the description field in the HTTP response body.
None data Any Optional per-instance override for the data field in the HTTP response body.
None","path":["Reference","exceptions"],"tags":[]},{"location":"reference/exceptions/#fastapi_toolsets.exceptions.exceptions.NotFoundError","level":2,"title":"fastapi_toolsets.exceptions.exceptions.NotFoundError","text":" Bases: ApiException
HTTP 404 - Resource not found.
","path":["Reference","exceptions"],"tags":[]},{"location":"reference/exceptions/#fastapi_toolsets.exceptions.exceptions.NotFoundError.__init__","level":3,"title":"__init__(detail=None, *, desc=None, data=None)","text":"Initialize the exception.
Parameters:
Name Type Description Default detail str | None Optional human-readable message
None desc str | None Optional per-instance override for the description field in the HTTP response body.
None data Any Optional per-instance override for the data field in the HTTP response body.
None","path":["Reference","exceptions"],"tags":[]},{"location":"reference/exceptions/#fastapi_toolsets.exceptions.exceptions.ConflictError","level":2,"title":"fastapi_toolsets.exceptions.exceptions.ConflictError","text":" Bases: ApiException
HTTP 409 - Resource conflict.
","path":["Reference","exceptions"],"tags":[]},{"location":"reference/exceptions/#fastapi_toolsets.exceptions.exceptions.ConflictError.__init__","level":3,"title":"__init__(detail=None, *, desc=None, data=None)","text":"Initialize the exception.
Parameters:
Name Type Description Default detail str | None Optional human-readable message
None desc str | None Optional per-instance override for the description field in the HTTP response body.
None data Any Optional per-instance override for the data field in the HTTP response body.
None","path":["Reference","exceptions"],"tags":[]},{"location":"reference/exceptions/#fastapi_toolsets.exceptions.exceptions.NoSearchableFieldsError","level":2,"title":"fastapi_toolsets.exceptions.exceptions.NoSearchableFieldsError","text":" Bases: ApiException
Raised when search is requested but no searchable fields are available.
","path":["Reference","exceptions"],"tags":[]},{"location":"reference/exceptions/#fastapi_toolsets.exceptions.exceptions.NoSearchableFieldsError.__init__","level":3,"title":"__init__(model)","text":"Initialize the exception.
Parameters:
Name Type Description Default model type The model class that has no searchable fields configured.
required","path":["Reference","exceptions"],"tags":[]},{"location":"reference/exceptions/#fastapi_toolsets.exceptions.exceptions.InvalidFacetFilterError","level":2,"title":"fastapi_toolsets.exceptions.exceptions.InvalidFacetFilterError","text":" Bases: ApiException
Raised when filter_by contains a key not declared in facet_fields.
","path":["Reference","exceptions"],"tags":[]},{"location":"reference/exceptions/#fastapi_toolsets.exceptions.exceptions.InvalidFacetFilterError.__init__","level":3,"title":"__init__(key, valid_keys)","text":"Initialize the exception.
Parameters:
Name Type Description Default key str The unknown filter key provided by the caller.
required valid_keys set[str] Set of valid keys derived from the declared facet_fields.
required","path":["Reference","exceptions"],"tags":[]},{"location":"reference/exceptions/#fastapi_toolsets.exceptions.exceptions.InvalidOrderFieldError","level":2,"title":"fastapi_toolsets.exceptions.exceptions.InvalidOrderFieldError","text":" Bases: ApiException
Raised when order_by contains a field not in the allowed order fields.
","path":["Reference","exceptions"],"tags":[]},{"location":"reference/exceptions/#fastapi_toolsets.exceptions.exceptions.InvalidOrderFieldError.__init__","level":3,"title":"__init__(field, valid_fields)","text":"Initialize the exception.
Parameters:
Name Type Description Default field str The unknown order field provided by the caller.
required valid_fields list[str] List of valid field names.
required","path":["Reference","exceptions"],"tags":[]},{"location":"reference/exceptions/#fastapi_toolsets.exceptions.exceptions.generate_error_responses","level":2,"title":"fastapi_toolsets.exceptions.exceptions.generate_error_responses(*errors)","text":"Generate OpenAPI response documentation for exceptions.
Parameters:
Name Type Description Default *errors type[ApiException] Exception classes that inherit from ApiException.
() Returns:
Type Description dict[int | str, dict[str, Any]] Dict suitable for FastAPI's responses parameter.
","path":["Reference","exceptions"],"tags":[]},{"location":"reference/exceptions/#fastapi_toolsets.exceptions.handler.init_exceptions_handlers","level":2,"title":"fastapi_toolsets.exceptions.handler.init_exceptions_handlers(app)","text":"Register exception handlers and custom OpenAPI schema on a FastAPI app.
Parameters:
Name Type Description Default app FastAPI FastAPI application instance.
required Returns:
Type Description FastAPI The same FastAPI instance (for chaining).
","path":["Reference","exceptions"],"tags":[]},{"location":"reference/fixtures/","level":1,"title":"fixtures","text":"Here's the reference for the fixture registry, enums, and loading utilities.
You can import them directly from fastapi_toolsets.fixtures:
from fastapi_toolsets.fixtures import (\n Context,\n LoadStrategy,\n Fixture,\n FixtureRegistry,\n load_fixtures,\n load_fixtures_by_context,\n get_obj_by_attr,\n)\n ","path":["Reference","fixtures"],"tags":[]},{"location":"reference/fixtures/#fastapi_toolsets.fixtures.enum.Context","level":2,"title":"fastapi_toolsets.fixtures.enum.Context","text":" Bases: str, Enum
Predefined fixture contexts.
","path":["Reference","fixtures"],"tags":[]},{"location":"reference/fixtures/#fastapi_toolsets.fixtures.enum.Context.BASE","level":3,"title":"BASE = 'base' class-attribute instance-attribute","text":"Base fixtures loaded in all environments.
","path":["Reference","fixtures"],"tags":[]},{"location":"reference/fixtures/#fastapi_toolsets.fixtures.enum.Context.DEVELOPMENT","level":3,"title":"DEVELOPMENT = 'development' class-attribute instance-attribute","text":"Development fixtures.
","path":["Reference","fixtures"],"tags":[]},{"location":"reference/fixtures/#fastapi_toolsets.fixtures.enum.Context.PRODUCTION","level":3,"title":"PRODUCTION = 'production' class-attribute instance-attribute","text":"Production-only fixtures.
","path":["Reference","fixtures"],"tags":[]},{"location":"reference/fixtures/#fastapi_toolsets.fixtures.enum.Context.TESTING","level":3,"title":"TESTING = 'testing' class-attribute instance-attribute","text":"Test fixtures.
","path":["Reference","fixtures"],"tags":[]},{"location":"reference/fixtures/#fastapi_toolsets.fixtures.enum.LoadStrategy","level":2,"title":"fastapi_toolsets.fixtures.enum.LoadStrategy","text":" Bases: str, Enum
Strategy for loading fixtures into the database.
","path":["Reference","fixtures"],"tags":[]},{"location":"reference/fixtures/#fastapi_toolsets.fixtures.enum.LoadStrategy.INSERT","level":3,"title":"INSERT = 'insert' class-attribute instance-attribute","text":"Insert new records. Fails if record already exists.
","path":["Reference","fixtures"],"tags":[]},{"location":"reference/fixtures/#fastapi_toolsets.fixtures.enum.LoadStrategy.MERGE","level":3,"title":"MERGE = 'merge' class-attribute instance-attribute","text":"Insert or update based on primary key (SQLAlchemy merge).
","path":["Reference","fixtures"],"tags":[]},{"location":"reference/fixtures/#fastapi_toolsets.fixtures.enum.LoadStrategy.SKIP_EXISTING","level":3,"title":"SKIP_EXISTING = 'skip_existing' class-attribute instance-attribute","text":"Insert only if record doesn't exist (based on primary key).
","path":["Reference","fixtures"],"tags":[]},{"location":"reference/fixtures/#fastapi_toolsets.fixtures.registry.Fixture","level":2,"title":"fastapi_toolsets.fixtures.registry.Fixture dataclass","text":"A fixture definition with metadata.
","path":["Reference","fixtures"],"tags":[]},{"location":"reference/fixtures/#fastapi_toolsets.fixtures.registry.FixtureRegistry","level":2,"title":"fastapi_toolsets.fixtures.registry.FixtureRegistry","text":"Registry for managing fixtures with dependencies.
Example from fastapi_toolsets.fixtures import FixtureRegistry, Context\n\nfixtures = FixtureRegistry()\n\n@fixtures.register\ndef roles():\n return [\n Role(id=1, name=\"admin\"),\n Role(id=2, name=\"user\"),\n ]\n\n@fixtures.register(depends_on=[\"roles\"])\ndef users():\n return [\n User(id=1, username=\"admin\", role_id=1),\n ]\n\n@fixtures.register(depends_on=[\"users\"], contexts=[Context.TESTING])\ndef test_data():\n return [\n Post(id=1, title=\"Test\", user_id=1),\n ]\n ","path":["Reference","fixtures"],"tags":[]},{"location":"reference/fixtures/#fastapi_toolsets.fixtures.registry.FixtureRegistry.get","level":3,"title":"get(name)","text":"Get a fixture by name.
","path":["Reference","fixtures"],"tags":[]},{"location":"reference/fixtures/#fastapi_toolsets.fixtures.registry.FixtureRegistry.get_all","level":3,"title":"get_all()","text":"Get all registered fixtures.
","path":["Reference","fixtures"],"tags":[]},{"location":"reference/fixtures/#fastapi_toolsets.fixtures.registry.FixtureRegistry.get_by_context","level":3,"title":"get_by_context(*contexts)","text":"Get fixtures for specific contexts.
","path":["Reference","fixtures"],"tags":[]},{"location":"reference/fixtures/#fastapi_toolsets.fixtures.registry.FixtureRegistry.include_registry","level":3,"title":"include_registry(registry)","text":"Include another FixtureRegistry in the same current FixtureRegistry.
Parameters:
Name Type Description Default registry FixtureRegistry The FixtureRegistry to include
required Raises:
Type Description ValueError If a fixture name already exists in the current registry
Example registry = FixtureRegistry()\ndev_registry = FixtureRegistry()\n\n@dev_registry.register\ndef dev_data():\n return [...]\n\nregistry.include_registry(registry=dev_registry)\n ","path":["Reference","fixtures"],"tags":[]},{"location":"reference/fixtures/#fastapi_toolsets.fixtures.registry.FixtureRegistry.register","level":3,"title":"register(func=None, *, name=None, depends_on=None, contexts=None)","text":"Register a fixture function.
Can be used as a decorator with or without arguments.
Parameters:
Name Type Description Default func Callable[[], Sequence[DeclarativeBase]] | None Fixture function returning list of model instances
None name str | None Fixture name (defaults to function name)
None depends_on list[str] | None List of fixture names this depends on
None contexts list[str | Context] | None List of contexts this fixture belongs to
None Example @fixtures.register\ndef roles():\n return [Role(id=1, name=\"admin\")]\n\n@fixtures.register(depends_on=[\"roles\"], contexts=[Context.TESTING])\ndef test_users():\n return [User(id=1, username=\"test\", role_id=1)]\n ","path":["Reference","fixtures"],"tags":[]},{"location":"reference/fixtures/#fastapi_toolsets.fixtures.registry.FixtureRegistry.resolve_context_dependencies","level":3,"title":"resolve_context_dependencies(*contexts)","text":"Resolve all fixtures for contexts with dependencies.
Parameters:
Name Type Description Default *contexts str | Context Contexts to load
() Returns:
Type Description list[str] List of fixture names in load order
","path":["Reference","fixtures"],"tags":[]},{"location":"reference/fixtures/#fastapi_toolsets.fixtures.registry.FixtureRegistry.resolve_dependencies","level":3,"title":"resolve_dependencies(*names)","text":"Resolve fixture dependencies in topological order.
Parameters:
Name Type Description Default *names str Fixture names to resolve
() Returns:
Type Description list[str] List of fixture names in load order (dependencies first)
Raises:
Type Description KeyError If a fixture is not found
ValueError If circular dependency detected
","path":["Reference","fixtures"],"tags":[]},{"location":"reference/fixtures/#fastapi_toolsets.fixtures.utils.load_fixtures","level":2,"title":"fastapi_toolsets.fixtures.utils.load_fixtures(session, registry, *names, strategy=LoadStrategy.MERGE) async","text":"Load specific fixtures by name with dependencies.
Parameters:
Name Type Description Default session AsyncSession Database session
required registry FixtureRegistry Fixture registry
required *names str Fixture names to load (dependencies auto-resolved)
() strategy LoadStrategy How to handle existing records
MERGE Returns:
Type Description dict[str, list[DeclarativeBase]] Dict mapping fixture names to loaded instances
","path":["Reference","fixtures"],"tags":[]},{"location":"reference/fixtures/#fastapi_toolsets.fixtures.utils.load_fixtures_by_context","level":2,"title":"fastapi_toolsets.fixtures.utils.load_fixtures_by_context(session, registry, *contexts, strategy=LoadStrategy.MERGE) async","text":"Load all fixtures for specific contexts.
Parameters:
Name Type Description Default session AsyncSession Database session
required registry FixtureRegistry Fixture registry
required *contexts str | Context Contexts to load (e.g., Context.BASE, Context.TESTING)
() strategy LoadStrategy How to handle existing records
MERGE Returns:
Type Description dict[str, list[DeclarativeBase]] Dict mapping fixture names to loaded instances
","path":["Reference","fixtures"],"tags":[]},{"location":"reference/fixtures/#fastapi_toolsets.fixtures.utils.get_obj_by_attr","level":2,"title":"fastapi_toolsets.fixtures.utils.get_obj_by_attr(fixtures, attr_name, value)","text":"Get a SQLAlchemy model instance by matching an attribute value.
Parameters:
Name Type Description Default fixtures Callable[[], Sequence[ModelType]] A fixture function registered via @registry.register that returns a sequence of SQLAlchemy model instances.
required attr_name str Name of the attribute to match against.
required value Any Value to match.
required Returns:
Type Description ModelType The first model instance where the attribute matches the given value.
Raises:
Type Description StopIteration If no matching object is found in the fixture group.
","path":["Reference","fixtures"],"tags":[]},{"location":"reference/logger/","level":1,"title":"logger","text":"Here's the reference for the logging utilities.
You can import them directly from fastapi_toolsets.logger:
from fastapi_toolsets.logger import configure_logging, get_logger\n ","path":["Reference","logger"],"tags":[]},{"location":"reference/logger/#fastapi_toolsets.logger.configure_logging","level":2,"title":"fastapi_toolsets.logger.configure_logging(level='INFO', fmt=DEFAULT_FORMAT, logger_name=None)","text":"Configure logging with a stdout handler and consistent format.
Sets up a :class:~logging.StreamHandler writing to stdout with the given format and level. Also configures the uvicorn loggers so that FastAPI access logs use the same format.
Calling this function multiple times is safe -- existing handlers are replaced rather than duplicated.
Parameters:
Name Type Description Default level LogLevel | int Log level (e.g. \"DEBUG\", \"INFO\", or logging.DEBUG).
'INFO' fmt str Log format string. Defaults to \"%(asctime)s - %(name)s - %(levelname)s - %(message)s\".
DEFAULT_FORMAT logger_name str | None Logger name to configure. None (the default) configures the root logger so all loggers inherit the settings.
None Returns:
Type Description Logger The configured Logger instance.
Example from fastapi_toolsets.logger import configure_logging\n\nlogger = configure_logging(\"DEBUG\")\nlogger.info(\"Application started\")\n ","path":["Reference","logger"],"tags":[]},{"location":"reference/logger/#fastapi_toolsets.logger.get_logger","level":2,"title":"fastapi_toolsets.logger.get_logger(name=_SENTINEL)","text":"Return a logger with the given name.
A thin convenience wrapper around :func:logging.getLogger that keeps logging imports consistent across the codebase.
When called without arguments, the caller's __name__ is used automatically, so get_logger() in a module is equivalent to logging.getLogger(__name__). Pass None explicitly to get the root logger.
Parameters:
Name Type Description Default name str | None Logger name. Defaults to the caller's __name__. Pass None to get the root logger.
_SENTINEL Returns:
Type Description Logger A Logger instance.
Example from fastapi_toolsets.logger import get_logger\n\nlogger = get_logger() # uses caller's __name__\nlogger = get_logger(\"myapp\") # explicit name\nlogger = get_logger(None) # root logger\n ","path":["Reference","logger"],"tags":[]},{"location":"reference/metrics/","level":1,"title":"metrics","text":"Here's the reference for the Prometheus metrics registry and endpoint handler.
You can import them directly from fastapi_toolsets.metrics:
from fastapi_toolsets.metrics import Metric, MetricsRegistry, init_metrics\n ","path":["Reference","metrics"],"tags":[]},{"location":"reference/metrics/#fastapi_toolsets.metrics.registry.Metric","level":2,"title":"fastapi_toolsets.metrics.registry.Metric dataclass","text":"A metric definition with metadata.
","path":["Reference","metrics"],"tags":[]},{"location":"reference/metrics/#fastapi_toolsets.metrics.registry.MetricsRegistry","level":2,"title":"fastapi_toolsets.metrics.registry.MetricsRegistry","text":"Registry for managing Prometheus metric providers and collectors.
","path":["Reference","metrics"],"tags":[]},{"location":"reference/metrics/#fastapi_toolsets.metrics.registry.MetricsRegistry.get","level":3,"title":"get(name)","text":"Return the metric instance created by a provider.
Parameters:
Name Type Description Default name str The metric name (defaults to the provider function name).
required Raises:
Type Description KeyError If the metric name is unknown or init_metrics has not been called yet.
","path":["Reference","metrics"],"tags":[]},{"location":"reference/metrics/#fastapi_toolsets.metrics.registry.MetricsRegistry.get_all","level":3,"title":"get_all()","text":"Get all registered metric definitions.
","path":["Reference","metrics"],"tags":[]},{"location":"reference/metrics/#fastapi_toolsets.metrics.registry.MetricsRegistry.get_collectors","level":3,"title":"get_collectors()","text":"Get collectors (called on each scrape).
","path":["Reference","metrics"],"tags":[]},{"location":"reference/metrics/#fastapi_toolsets.metrics.registry.MetricsRegistry.get_providers","level":3,"title":"get_providers()","text":"Get metric providers (called once at init).
","path":["Reference","metrics"],"tags":[]},{"location":"reference/metrics/#fastapi_toolsets.metrics.registry.MetricsRegistry.include_registry","level":3,"title":"include_registry(registry)","text":"Include another :class:MetricsRegistry into this one.
Parameters:
Name Type Description Default registry MetricsRegistry The registry to merge in.
required Raises:
Type Description ValueError If a metric name already exists in the current registry.
","path":["Reference","metrics"],"tags":[]},{"location":"reference/metrics/#fastapi_toolsets.metrics.registry.MetricsRegistry.register","level":3,"title":"register(func=None, *, name=None, collect=False)","text":"Register a metric provider or collector function.
Can be used as a decorator with or without arguments.
Parameters:
Name Type Description Default func Callable[..., Any] | None The metric function to register.
None name str | None Metric name (defaults to function name).
None collect bool If True, the function is called on every scrape. If False (default), called once at init time.
False","path":["Reference","metrics"],"tags":[]},{"location":"reference/metrics/#fastapi_toolsets.metrics.handler.init_metrics","level":2,"title":"fastapi_toolsets.metrics.handler.init_metrics(app, registry, *, path='/metrics')","text":"Register a Prometheus /metrics endpoint on a FastAPI app.
Parameters:
Name Type Description Default app FastAPI FastAPI application instance.
required registry MetricsRegistry A :class:MetricsRegistry containing providers and collectors.
required path str URL path for the metrics endpoint (default /metrics).
'/metrics' Returns:
Type Description FastAPI The same FastAPI instance (for chaining).
Example from fastapi import FastAPI\nfrom fastapi_toolsets.metrics import MetricsRegistry, init_metrics\n\nmetrics = MetricsRegistry()\napp = FastAPI()\ninit_metrics(app, registry=metrics)\n ","path":["Reference","metrics"],"tags":[]},{"location":"reference/models/","level":1,"title":"models","text":"Here's the reference for the SQLAlchemy model mixins provided by the models module.
You can import them directly from fastapi_toolsets.models:
from fastapi_toolsets.models import (\n UUIDMixin,\n CreatedAtMixin,\n UpdatedAtMixin,\n TimestampMixin,\n)\n ","path":["Reference","models"],"tags":[]},{"location":"reference/models/#fastapi_toolsets.models.UUIDMixin","level":2,"title":"fastapi_toolsets.models.UUIDMixin","text":"Mixin that adds a UUID primary key auto-generated by the database.
","path":["Reference","models"],"tags":[]},{"location":"reference/models/#fastapi_toolsets.models.CreatedAtMixin","level":2,"title":"fastapi_toolsets.models.CreatedAtMixin","text":"Mixin that adds a created_at timestamp column.
","path":["Reference","models"],"tags":[]},{"location":"reference/models/#fastapi_toolsets.models.UpdatedAtMixin","level":2,"title":"fastapi_toolsets.models.UpdatedAtMixin","text":"Mixin that adds an updated_at timestamp column.
","path":["Reference","models"],"tags":[]},{"location":"reference/models/#fastapi_toolsets.models.TimestampMixin","level":2,"title":"fastapi_toolsets.models.TimestampMixin","text":" Bases: CreatedAtMixin, UpdatedAtMixin
Mixin that combines created_at and updated_at timestamp columns.
","path":["Reference","models"],"tags":[]},{"location":"reference/pytest/","level":1,"title":"pytest","text":"Here's the reference for all testing utilities and pytest fixtures.
You can import them directly from fastapi_toolsets.pytest:
from fastapi_toolsets.pytest import (\n register_fixtures,\n create_async_client,\n create_db_session,\n worker_database_url,\n create_worker_database,\n cleanup_tables,\n)\n ","path":["Reference","pytest"],"tags":[]},{"location":"reference/pytest/#fastapi_toolsets.pytest.plugin.register_fixtures","level":2,"title":"fastapi_toolsets.pytest.plugin.register_fixtures(registry, namespace, *, prefix='fixture_', session_fixture='db_session', strategy=LoadStrategy.MERGE)","text":"Register pytest fixtures from a FixtureRegistry.
Automatically creates pytest fixtures for each fixture in the registry. Dependencies are resolved via pytest fixture dependencies.
Parameters:
Name Type Description Default registry FixtureRegistry The FixtureRegistry containing fixtures
required namespace dict[str, Any] The module's globals() dict to add fixtures to
required prefix str Prefix for generated fixture names (default: \"fixture_\")
'fixture_' session_fixture str Name of the db session fixture (default: \"db_session\")
'db_session' strategy LoadStrategy Loading strategy for fixtures (default: MERGE)
MERGE Returns:
Type Description list[str] List of created fixture names
Example # conftest.py\nfrom app.fixtures import fixtures\nfrom fastapi_toolsets.pytest_plugin import register_fixtures\n\nregister_fixtures(fixtures, globals())\n\n# Creates fixtures like:\n# - fixture_roles\n# - fixture_users (depends on fixture_roles if users depends on roles)\n# - fixture_posts (depends on fixture_users if posts depends on users)\n ","path":["Reference","pytest"],"tags":[]},{"location":"reference/pytest/#fastapi_toolsets.pytest.utils.create_async_client","level":2,"title":"fastapi_toolsets.pytest.utils.create_async_client(app, base_url='http://test', dependency_overrides=None) async","text":"Create an async httpx client for testing FastAPI applications.
Parameters:
Name Type Description Default app Any FastAPI application instance.
required base_url str Base URL for requests. Defaults to \"http://test\".
'http://test' dependency_overrides dict[Callable[..., Any], Callable[..., Any]] | None Optional mapping of original dependencies to their test replacements. Applied via app.dependency_overrides before yielding and cleaned up after.
None Yields:
Type Description AsyncGenerator[AsyncClient, None] An AsyncClient configured for the app.
Example from fastapi import FastAPI\nfrom fastapi_toolsets.pytest import create_async_client\n\napp = FastAPI()\n\n@pytest.fixture\nasync def client():\n async with create_async_client(app) as c:\n yield c\n\nasync def test_endpoint(client: AsyncClient):\n response = await client.get(\"/health\")\n assert response.status_code == 200\n Example with dependency overrides from fastapi_toolsets.pytest import create_async_client, create_db_session\nfrom app.db import get_db\n\n@pytest.fixture\nasync def db_session():\n async with create_db_session(DATABASE_URL, Base, cleanup=True) as session:\n yield session\n\n@pytest.fixture\nasync def client(db_session):\n async def override():\n yield db_session\n\n async with create_async_client(\n app, dependency_overrides={get_db: override}\n ) as c:\n yield c\n ","path":["Reference","pytest"],"tags":[]},{"location":"reference/pytest/#fastapi_toolsets.pytest.utils.create_db_session","level":2,"title":"fastapi_toolsets.pytest.utils.create_db_session(database_url, base, *, echo=False, expire_on_commit=False, drop_tables=True, cleanup=False) async","text":"Create a database session for testing.
Creates tables before yielding the session and optionally drops them after. Each call creates a fresh engine and session for test isolation.
Parameters:
Name Type Description Default database_url str Database connection URL (e.g., \"postgresql+asyncpg://...\").
required base type[DeclarativeBase] SQLAlchemy DeclarativeBase class containing model metadata.
required echo bool Enable SQLAlchemy query logging. Defaults to False.
False expire_on_commit bool Expire objects after commit. Defaults to False.
False drop_tables bool Drop tables after test. Defaults to True.
True cleanup bool Truncate all tables after test using :func:cleanup_tables. Defaults to False.
False Yields:
Type Description AsyncGenerator[AsyncSession, None] An AsyncSession ready for database operations.
Example from fastapi_toolsets.pytest import create_db_session\nfrom app.models import Base\n\nDATABASE_URL = \"postgresql+asyncpg://user:pass@localhost/test_db\"\n\n@pytest.fixture\nasync def db_session():\n async with create_db_session(\n DATABASE_URL, Base, cleanup=True\n ) as session:\n yield session\n\nasync def test_create_user(db_session: AsyncSession):\n user = User(name=\"test\")\n db_session.add(user)\n await db_session.commit()\n ","path":["Reference","pytest"],"tags":[]},{"location":"reference/pytest/#fastapi_toolsets.pytest.utils.worker_database_url","level":2,"title":"fastapi_toolsets.pytest.utils.worker_database_url(database_url, default_test_db)","text":"Derive a per-worker database URL for pytest-xdist parallel runs.
Appends _{worker_name} to the database name so each xdist worker operates on its own database. When not running under xdist, _{default_test_db} is appended instead.
The worker name is read from the PYTEST_XDIST_WORKER environment variable (set automatically by xdist in each worker process).
Parameters:
Name Type Description Default database_url str Original database connection URL.
required default_test_db str Suffix appended to the database name when PYTEST_XDIST_WORKER is not set.
required Returns:
Type Description str A database URL with a worker- or default-specific database name.
","path":["Reference","pytest"],"tags":[]},{"location":"reference/pytest/#fastapi_toolsets.pytest.utils.create_worker_database","level":2,"title":"fastapi_toolsets.pytest.utils.create_worker_database(database_url, default_test_db='test_db') async","text":"Create and drop a per-worker database for pytest-xdist isolation.
Derives a worker-specific database URL using :func:worker_database_url, then delegates to :func:~fastapi_toolsets.db.create_database to create and drop it. Intended for use as a session-scoped fixture.
When running under xdist the database name is suffixed with the worker name (e.g. _gw0). Otherwise it is suffixed with default_test_db.
Parameters:
Name Type Description Default database_url str Original database connection URL (used as the server connection and as the base for the worker database name).
required default_test_db str Suffix appended to the database name when PYTEST_XDIST_WORKER is not set. Defaults to \"test_db\".
'test_db' Yields:
Type Description AsyncGenerator[str, None] The worker-specific database URL.
Example from fastapi_toolsets.pytest import create_worker_database, create_db_session\n\nDATABASE_URL = \"postgresql+asyncpg://postgres:postgres@localhost/test_db\"\n\n@pytest.fixture(scope=\"session\")\nasync def worker_db_url():\n async with create_worker_database(DATABASE_URL) as url:\n yield url\n\n@pytest.fixture\nasync def db_session(worker_db_url):\n async with create_db_session(\n worker_db_url, Base, cleanup=True\n ) as session:\n yield session\n ","path":["Reference","pytest"],"tags":[]},{"location":"reference/schemas/","level":1,"title":"schemas","text":"Here's the reference for all response models and types provided by the schemas module.
You can import them directly from fastapi_toolsets.schemas:
from fastapi_toolsets.schemas import (\n PydanticBase,\n ResponseStatus,\n ApiError,\n BaseResponse,\n Response,\n ErrorResponse,\n OffsetPagination,\n CursorPagination,\n PaginatedResponse,\n)\n ","path":["Reference","schemas"],"tags":[]},{"location":"reference/schemas/#fastapi_toolsets.schemas.PydanticBase","level":2,"title":"fastapi_toolsets.schemas.PydanticBase","text":" Bases: BaseModel
Base class for all Pydantic models with common configuration.
","path":["Reference","schemas"],"tags":[]},{"location":"reference/schemas/#fastapi_toolsets.schemas.ResponseStatus","level":2,"title":"fastapi_toolsets.schemas.ResponseStatus","text":" Bases: str, Enum
Standard API response status.
","path":["Reference","schemas"],"tags":[]},{"location":"reference/schemas/#fastapi_toolsets.schemas.ApiError","level":2,"title":"fastapi_toolsets.schemas.ApiError","text":" Bases: PydanticBase
Structured API error definition.
Used to define standard error responses with consistent format.
Attributes:
Name Type Description code int HTTP status code
msg str Short error message
desc str Detailed error description
err_code str Application-specific error code (e.g., \"AUTH-401\")
","path":["Reference","schemas"],"tags":[]},{"location":"reference/schemas/#fastapi_toolsets.schemas.BaseResponse","level":2,"title":"fastapi_toolsets.schemas.BaseResponse","text":" Bases: PydanticBase
Base response structure for all API responses.
Attributes:
Name Type Description status ResponseStatus SUCCESS or FAIL
message str Human-readable message
error_code str | None Error code if status is FAIL, None otherwise
","path":["Reference","schemas"],"tags":[]},{"location":"reference/schemas/#fastapi_toolsets.schemas.Response","level":2,"title":"fastapi_toolsets.schemas.Response","text":" Bases: BaseResponse, Generic[DataT]
Generic API response with data payload.
Example Response[UserRead](data=user, message=\"User retrieved\")\n ","path":["Reference","schemas"],"tags":[]},{"location":"reference/schemas/#fastapi_toolsets.schemas.ErrorResponse","level":2,"title":"fastapi_toolsets.schemas.ErrorResponse","text":" Bases: BaseResponse
Error response with additional description field.
Used for error responses that need more context.
","path":["Reference","schemas"],"tags":[]},{"location":"reference/schemas/#fastapi_toolsets.schemas.OffsetPagination","level":2,"title":"fastapi_toolsets.schemas.OffsetPagination","text":" Bases: PydanticBase
Pagination metadata for offset-based list responses.
Attributes:
Name Type Description total_count int Total number of items across all pages
items_per_page int Number of items per page
page int Current page number (1-indexed)
has_more bool Whether there are more pages
","path":["Reference","schemas"],"tags":[]},{"location":"reference/schemas/#fastapi_toolsets.schemas.CursorPagination","level":2,"title":"fastapi_toolsets.schemas.CursorPagination","text":" Bases: PydanticBase
Pagination metadata for cursor-based list responses.
Attributes:
Name Type Description next_cursor str | None Encoded cursor for the next page, or None on the last page.
prev_cursor str | None Encoded cursor for the previous page, or None on the first page.
items_per_page int Number of items requested per page.
has_more bool Whether there is at least one more page after this one.
","path":["Reference","schemas"],"tags":[]},{"location":"reference/schemas/#fastapi_toolsets.schemas.PaginatedResponse","level":2,"title":"fastapi_toolsets.schemas.PaginatedResponse","text":" Bases: BaseResponse, Generic[DataT]
Paginated API response for list endpoints.
","path":["Reference","schemas"],"tags":[]}]}
\ No newline at end of file
diff --git a/v2.2/sitemap.xml b/v2.2/sitemap.xml
new file mode 100644
index 0000000..87383c5
--- /dev/null
+++ b/v2.2/sitemap.xml
@@ -0,0 +1,78 @@
+
+
+
+ https://fastapi-toolsets.d3vyce.fr/v2.2/
+
+
+ https://fastapi-toolsets.d3vyce.fr/v2.2/module/cli/
+
+
+ https://fastapi-toolsets.d3vyce.fr/v2.2/module/crud/
+
+
+ https://fastapi-toolsets.d3vyce.fr/v2.2/module/db/
+
+
+ https://fastapi-toolsets.d3vyce.fr/v2.2/module/dependencies/
+
+
+ https://fastapi-toolsets.d3vyce.fr/v2.2/module/exceptions/
+
+
+ https://fastapi-toolsets.d3vyce.fr/v2.2/module/fixtures/
+
+
+ https://fastapi-toolsets.d3vyce.fr/v2.2/module/logger/
+
+
+ https://fastapi-toolsets.d3vyce.fr/v2.2/module/metrics/
+
+
+ https://fastapi-toolsets.d3vyce.fr/v2.2/module/models/
+
+
+ https://fastapi-toolsets.d3vyce.fr/v2.2/module/pytest/
+
+
+ https://fastapi-toolsets.d3vyce.fr/v2.2/module/schemas/
+
+
+ https://fastapi-toolsets.d3vyce.fr/v2.2/reference/cli/
+
+
+ https://fastapi-toolsets.d3vyce.fr/v2.2/reference/crud/
+
+
+ https://fastapi-toolsets.d3vyce.fr/v2.2/reference/db/
+
+
+ https://fastapi-toolsets.d3vyce.fr/v2.2/reference/dependencies/
+
+
+ https://fastapi-toolsets.d3vyce.fr/v2.2/reference/exceptions/
+
+
+ https://fastapi-toolsets.d3vyce.fr/v2.2/reference/fixtures/
+
+
+ https://fastapi-toolsets.d3vyce.fr/v2.2/reference/logger/
+
+
+ https://fastapi-toolsets.d3vyce.fr/v2.2/reference/metrics/
+
+
+ https://fastapi-toolsets.d3vyce.fr/v2.2/reference/models/
+
+
+ https://fastapi-toolsets.d3vyce.fr/v2.2/reference/pytest/
+
+
+ https://fastapi-toolsets.d3vyce.fr/v2.2/reference/schemas/
+
+
+ https://fastapi-toolsets.d3vyce.fr/v2.2/examples/pagination-search/
+
+
+ https://fastapi-toolsets.d3vyce.fr/v2.2/migration/v2/
+
+
\ No newline at end of file
diff --git a/versions.json b/versions.json
index 9b6fafd..55f8b63 100644
--- a/versions.json
+++ b/versions.json
@@ -36,6 +36,11 @@
"title": "1.0",
"aliases": []
},
+ {
+ "version": "v2.2",
+ "title": "v2.2",
+ "aliases": []
+ },
{
"version": "v2.1",
"title": "v2.1",