From eca29a5bdb320feff08dd915c3d2da7267e2d369 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 28 Jul 2026 12:24:25 +0000 Subject: [PATCH] Deployed 88ecfa8 to v5.1 with Zensical 0.0.51 and mike 2.2.0+zensical-0.1.0 --- stable | 2 +- v5.1/404.html | 1604 ++++ v5.1/assets/images/favicon.png | Bin 0 -> 1870 bytes v5.1/assets/javascripts/LICENSE | 29 + .../assets/javascripts/bundle.d7f30b55.min.js | 10 + .../workers/search.b6b7e04f.min.js | 1 + .../stylesheets/classic/main.f62f0af6.min.css | 1 + .../classic/palette.7dc9a0ad.min.css | 1 + .../stylesheets/modern/main.62c9b9cc.min.css | 1 + .../modern/palette.dfe2e883.min.css | 1 + v5.1/examples/pagination-search/index.html | 2306 ++++++ v5.1/index.html | 1915 +++++ v5.1/migration/v2/index.html | 2153 ++++++ v5.1/migration/v3/index.html | 2145 ++++++ v5.1/migration/v4/index.html | 1894 +++++ v5.1/migration/v5/index.html | 2185 ++++++ v5.1/module/cli/index.html | 2042 +++++ v5.1/module/crud/index.html | 3126 ++++++++ v5.1/module/db/index.html | 2360 ++++++ v5.1/module/dependencies/index.html | 1932 +++++ v5.1/module/exceptions/index.html | 2186 ++++++ v5.1/module/fixtures/index.html | 2290 ++++++ v5.1/module/logger/index.html | 1904 +++++ v5.1/module/metrics/index.html | 2088 ++++++ v5.1/module/models/index.html | 2360 ++++++ v5.1/module/pytest/index.html | 2065 +++++ v5.1/module/schemas/index.html | 2172 ++++++ v5.1/objects.inv | Bin 0 -> 1937 bytes v5.1/overrides/main.html | 7 + v5.1/reference/cli/index.html | 2253 ++++++ v5.1/reference/crud/index.html | 6655 +++++++++++++++++ v5.1/reference/db/index.html | 4206 +++++++++++ v5.1/reference/dependencies/index.html | 2175 ++++++ v5.1/reference/exceptions/index.html | 3992 ++++++++++ v5.1/reference/fixtures/index.html | 4036 ++++++++++ v5.1/reference/logger/index.html | 2074 +++++ v5.1/reference/metrics/index.html | 2524 +++++++ v5.1/reference/models/index.html | 2400 ++++++ v5.1/reference/pytest/index.html | 2743 +++++++ v5.1/reference/schemas/index.html | 2987 ++++++++ v5.1/search.json | 1 + v5.1/sitemap.xml | 87 + versions.json | 9 +- 43 files changed, 74919 insertions(+), 3 deletions(-) create mode 100644 v5.1/404.html create mode 100644 v5.1/assets/images/favicon.png create mode 100644 v5.1/assets/javascripts/LICENSE create mode 100644 v5.1/assets/javascripts/bundle.d7f30b55.min.js create mode 100644 v5.1/assets/javascripts/workers/search.b6b7e04f.min.js create mode 100644 v5.1/assets/stylesheets/classic/main.f62f0af6.min.css create mode 100644 v5.1/assets/stylesheets/classic/palette.7dc9a0ad.min.css create mode 100644 v5.1/assets/stylesheets/modern/main.62c9b9cc.min.css create mode 100644 v5.1/assets/stylesheets/modern/palette.dfe2e883.min.css create mode 100644 v5.1/examples/pagination-search/index.html create mode 100644 v5.1/index.html create mode 100644 v5.1/migration/v2/index.html create mode 100644 v5.1/migration/v3/index.html create mode 100644 v5.1/migration/v4/index.html create mode 100644 v5.1/migration/v5/index.html create mode 100644 v5.1/module/cli/index.html create mode 100644 v5.1/module/crud/index.html create mode 100644 v5.1/module/db/index.html create mode 100644 v5.1/module/dependencies/index.html create mode 100644 v5.1/module/exceptions/index.html create mode 100644 v5.1/module/fixtures/index.html create mode 100644 v5.1/module/logger/index.html create mode 100644 v5.1/module/metrics/index.html create mode 100644 v5.1/module/models/index.html create mode 100644 v5.1/module/pytest/index.html create mode 100644 v5.1/module/schemas/index.html create mode 100644 v5.1/objects.inv create mode 100644 v5.1/overrides/main.html create mode 100644 v5.1/reference/cli/index.html create mode 100644 v5.1/reference/crud/index.html create mode 100644 v5.1/reference/db/index.html create mode 100644 v5.1/reference/dependencies/index.html create mode 100644 v5.1/reference/exceptions/index.html create mode 100644 v5.1/reference/fixtures/index.html create mode 100644 v5.1/reference/logger/index.html create mode 100644 v5.1/reference/metrics/index.html create mode 100644 v5.1/reference/models/index.html create mode 100644 v5.1/reference/pytest/index.html create mode 100644 v5.1/reference/schemas/index.html create mode 100644 v5.1/search.json create mode 100644 v5.1/sitemap.xml diff --git a/stable b/stable index a14d85c..d012306 120000 --- a/stable +++ b/stable @@ -1 +1 @@ -v5.0 \ No newline at end of file +v5.1 \ No newline at end of file diff --git a/v5.1/404.html b/v5.1/404.html new file mode 100644 index 0000000..0922268 --- /dev/null +++ b/v5.1/404.html @@ -0,0 +1,1604 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + FastAPI Toolsets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + Skip to content + + +
+
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+ + + + + + +
+ + + + +
+
+
+ + + +
+ + + + + + + + + +
+ +

404 - Not found

+ +
+
+ + + + + +
+ + + +
+ +
+ + + + +
+ +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/v5.1/assets/images/favicon.png b/v5.1/assets/images/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..1cf13b9f9d978896599290a74f77d5dbe7d1655c GIT binary patch literal 1870 zcmV-U2eJ5xP)Gc)JR9QMau)O=X#!i9;T z37kk-upj^(fsR36MHs_+1RCI)NNu9}lD0S{B^g8PN?Ww(5|~L#Ng*g{WsqleV}|#l zz8@ri&cTzw_h33bHI+12+kK6WN$h#n5cD8OQt`5kw6p~9H3()bUQ8OS4Q4HTQ=1Ol z_JAocz`fLbT2^{`8n~UAo=#AUOf=SOq4pYkt;XbC&f#7lb$*7=$na!mWCQ`dBQsO0 zLFBSPj*N?#u5&pf2t4XjEGH|=pPQ8xh7tpx;US5Cx_Ju;!O`ya-yF`)b%TEt5>eP1ZX~}sjjA%FJF?h7cX8=b!DZl<6%Cv z*G0uvvU+vmnpLZ2paivG-(cd*y3$hCIcsZcYOGh{$&)A6*XX&kXZd3G8m)G$Zz-LV z^GF3VAW^Mdv!)4OM8EgqRiz~*Cji;uzl2uC9^=8I84vNp;ltJ|q-*uQwGp2ma6cY7 z;`%`!9UXO@fr&Ebapfs34OmS9^u6$)bJxrucutf>`dKPKT%%*d3XlFVKunp9 zasduxjrjs>f8V=D|J=XNZp;_Zy^WgQ$9WDjgY=z@stwiEBm9u5*|34&1Na8BMjjgf3+SHcr`5~>oz1Y?SW^=K z^bTyO6>Gar#P_W2gEMwq)ot3; zREHn~U&Dp0l6YT0&k-wLwYjb?5zGK`W6S2v+K>AM(95m2C20L|3m~rN8dprPr@t)5lsk9Hu*W z?pS990s;Ez=+Rj{x7p``4>+c0G5^pYnB1^!TL=(?HLHZ+HicG{~4F1d^5Awl_2!1jICM-!9eoLhbbT^;yHcefyTAaqRcY zmuctDopPT!%k+}x%lZRKnzykr2}}XfG_ne?nRQO~?%hkzo;@RN{P6o`&mMUWBYMTe z6i8ChtjX&gXl`nvrU>jah)2iNM%JdjqoaeaU%yVn!^70x-flljp6Q5tK}5}&X8&&G zX3fpb3E(!rH=zVI_9Gjl45w@{(ITqngWFe7@9{mX;tO25Z_8 zQHEpI+FkTU#4xu>RkN>b3Tnc3UpWzPXWm#o55GKF09j^Mh~)K7{QqbO_~(@CVq! zS<8954|P8mXN2MRs86xZ&Q4EfM@JB94b=(YGuk)s&^jiSF=t3*oNK3`rD{H`yQ?d; ztE=laAUoZx5?RC8*WKOj`%LXEkgDd>&^Q4M^z`%u0rg-It=hLCVsq!Z%^6eB-OvOT zFZ28TN&cRmgU}Elrnk43)!>Z1FCPL2K$7}gwzIc48NX}#!A1BpJP?#v5wkNprhV** z?Cpalt1oH&{r!o3eSKc&ap)iz2BTn_VV`4>9M^b3;(YY}4>#ML6{~(4mH+?%07*qo IM6N<$f(jP3KmY&$ literal 0 HcmV?d00001 diff --git a/v5.1/assets/javascripts/LICENSE b/v5.1/assets/javascripts/LICENSE new file mode 100644 index 0000000..baab16b --- /dev/null +++ b/v5.1/assets/javascripts/LICENSE @@ -0,0 +1,29 @@ +------------------------------------------------------------------------------- +Third-Party licenses +------------------------------------------------------------------------------- + +Package: clipboard@2.0.11 +License: MIT +Copyright: Zeno Rocha + +------------------------------------------------------------------------------- + +Package: escape-html@1.0.3 +License: MIT +Copyright: 2012-2013 TJ Holowaychuk + 2015 Andreas Lubbe + 2015 Tiancheng "Timothy" Gu + +------------------------------------------------------------------------------- + +Package: focus-visible@5.2.1 +License: W3C +Copyright: WICG + +------------------------------------------------------------------------------- + +Package: rxjs@7.8.2 +License: Apache-2.0 +Copyright: 2015-2018 Google, Inc., + 2015-2018 Netflix, Inc., + 2015-2018 Microsoft Corp. and contributors diff --git a/v5.1/assets/javascripts/bundle.d7f30b55.min.js b/v5.1/assets/javascripts/bundle.d7f30b55.min.js new file mode 100644 index 0000000..a77dee7 --- /dev/null +++ b/v5.1/assets/javascripts/bundle.d7f30b55.min.js @@ -0,0 +1,10 @@ +"use strict";(()=>{var Nc=Object.create;var Rn=Object.defineProperty,Dc=Object.defineProperties,Wc=Object.getOwnPropertyDescriptor,Vc=Object.getOwnPropertyDescriptors,zc=Object.getOwnPropertyNames,zr=Object.getOwnPropertySymbols,qc=Object.getPrototypeOf,jn=Object.prototype.hasOwnProperty,zo=Object.prototype.propertyIsEnumerable;var Vo=(e,t,r)=>t in e?Rn(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,k=(e,t)=>{for(var r in t||(t={}))jn.call(t,r)&&Vo(e,r,t[r]);if(zr)for(var r of zr(t))zo.call(t,r)&&Vo(e,r,t[r]);return e},He=(e,t)=>Dc(e,Vc(t));var _r=(e,t)=>{var r={};for(var n in e)jn.call(e,n)&&t.indexOf(n)<0&&(r[n]=e[n]);if(e!=null&&zr)for(var n of zr(e))t.indexOf(n)<0&&zo.call(e,n)&&(r[n]=e[n]);return r};var Fn=(e,t)=>()=>{try{return t||e((t={exports:{}}).exports,t),t.exports}catch(r){throw t=0,r}};var Kc=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of zc(t))!jn.call(e,o)&&o!==r&&Rn(e,o,{get:()=>t[o],enumerable:!(n=Wc(t,o))||n.enumerable});return e};var xr=(e,t,r)=>(r=e!=null?Nc(qc(e)):{},Kc(t||!e||!e.__esModule?Rn(r,"default",{value:e,enumerable:!0}):r,e));var ut=(e,t,r)=>new Promise((n,o)=>{var i=c=>{try{s(r.next(c))}catch(l){o(l)}},a=c=>{try{s(r.throw(c))}catch(l){o(l)}},s=c=>c.done?n(c.value):Promise.resolve(c.value).then(i,a);s((r=r.apply(e,t)).next())});var Ko=Fn((Un,qo)=>{(function(e,t){typeof Un=="object"&&typeof qo!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(Un,(function(){"use strict";function e(r){var n=!0,o=!1,i=null,a={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function s(g){return!!(g&&g!==document&&g.nodeName!=="HTML"&&g.nodeName!=="BODY"&&"classList"in g&&"contains"in g.classList)}function c(g){var ee=g.type,ue=g.tagName;return!!(ue==="INPUT"&&a[ee]&&!g.readOnly||ue==="TEXTAREA"&&!g.readOnly||g.isContentEditable)}function l(g){g.classList.contains("focus-visible")||(g.classList.add("focus-visible"),g.setAttribute("data-focus-visible-added",""))}function u(g){g.hasAttribute("data-focus-visible-added")&&(g.classList.remove("focus-visible"),g.removeAttribute("data-focus-visible-added"))}function f(g){g.metaKey||g.altKey||g.ctrlKey||(s(r.activeElement)&&l(r.activeElement),n=!0)}function p(g){n=!1}function m(g){s(g.target)&&(n||c(g.target))&&l(g.target)}function h(g){s(g.target)&&(g.target.classList.contains("focus-visible")||g.target.hasAttribute("data-focus-visible-added"))&&(o=!0,window.clearTimeout(i),i=window.setTimeout(function(){o=!1},100),u(g.target))}function v(g){document.visibilityState==="hidden"&&(o&&(n=!0),_())}function _(){document.addEventListener("mousemove",x),document.addEventListener("mousedown",x),document.addEventListener("mouseup",x),document.addEventListener("pointermove",x),document.addEventListener("pointerdown",x),document.addEventListener("pointerup",x),document.addEventListener("touchmove",x),document.addEventListener("touchstart",x),document.addEventListener("touchend",x)}function S(){document.removeEventListener("mousemove",x),document.removeEventListener("mousedown",x),document.removeEventListener("mouseup",x),document.removeEventListener("pointermove",x),document.removeEventListener("pointerdown",x),document.removeEventListener("pointerup",x),document.removeEventListener("touchmove",x),document.removeEventListener("touchstart",x),document.removeEventListener("touchend",x)}function x(g){g.target.nodeName&&g.target.nodeName.toLowerCase()==="html"||(n=!1,S())}document.addEventListener("keydown",f,!0),document.addEventListener("mousedown",p,!0),document.addEventListener("pointerdown",p,!0),document.addEventListener("touchstart",p,!0),document.addEventListener("visibilitychange",v,!0),_(),r.addEventListener("focus",m,!0),r.addEventListener("blur",h,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)}))});var ko=Fn((lE,Ms)=>{"use strict";var fp=/["'&<>]/;Ms.exports=mp;function mp(e){var t=""+e,r=fp.exec(t);if(!r)return t;var n,o="",i=0,a=0;for(i=r.index;i{(function(t,r){typeof Ur=="object"&&typeof Co=="object"?Co.exports=r():typeof define=="function"&&define.amd?define([],r):typeof Ur=="object"?Ur.ClipboardJS=r():t.ClipboardJS=r()})(Ur,function(){return(function(){var e={686:(function(n,o,i){"use strict";i.d(o,{default:function(){return gr}});var a=i(279),s=i.n(a),c=i(370),l=i.n(c),u=i(817),f=i.n(u);function p(G){try{return document.execCommand(G)}catch(H){return!1}}var m=function(H){var C=f()(H);return p("cut"),C},h=m;function v(G){var H=document.documentElement.getAttribute("dir")==="rtl",C=document.createElement("textarea");C.style.fontSize="12pt",C.style.border="0",C.style.padding="0",C.style.margin="0",C.style.position="absolute",C.style[H?"right":"left"]="-9999px";var V=window.pageYOffset||document.documentElement.scrollTop;return C.style.top="".concat(V,"px"),C.setAttribute("readonly",""),C.value=G,C}var _=function(H,C){var V=v(H);C.container.appendChild(V);var z=f()(V);return p("copy"),V.remove(),z},S=function(H){var C=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},V="";return typeof H=="string"?V=_(H,C):H instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(H==null?void 0:H.type)?V=_(H.value,C):(V=f()(H),p("copy")),V},x=S;function g(G){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?g=function(C){return typeof C}:g=function(C){return C&&typeof Symbol=="function"&&C.constructor===Symbol&&C!==Symbol.prototype?"symbol":typeof C},g(G)}var ee=function(){var H=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},C=H.action,V=C===void 0?"copy":C,z=H.container,Z=H.target,We=H.text;if(V!=="copy"&&V!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(Z!==void 0)if(Z&&g(Z)==="object"&&Z.nodeType===1){if(V==="copy"&&Z.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(V==="cut"&&(Z.hasAttribute("readonly")||Z.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(We)return x(We,{container:z});if(Z)return V==="cut"?h(Z):x(Z,{container:z})},ue=ee;function L(G){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?L=function(C){return typeof C}:L=function(C){return C&&typeof Symbol=="function"&&C.constructor===Symbol&&C!==Symbol.prototype?"symbol":typeof C},L(G)}function M(G,H){if(!(G instanceof H))throw new TypeError("Cannot call a class as a function")}function W(G,H){for(var C=0;C0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof z.action=="function"?z.action:this.defaultAction,this.target=typeof z.target=="function"?z.target:this.defaultTarget,this.text=typeof z.text=="function"?z.text:this.defaultText,this.container=L(z.container)==="object"?z.container:document.body}},{key:"listenClick",value:function(z){var Z=this;this.listener=l()(z,"click",function(We){return Z.onClick(We)})}},{key:"onClick",value:function(z){var Z=z.delegateTarget||z.currentTarget,We=this.action(Z)||"copy",Xt=ue({action:We,container:this.container,target:this.target(Z),text:this.text(Z)});this.emit(Xt?"success":"error",{action:We,text:Xt,trigger:Z,clearSelection:function(){Z&&Z.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(z){return Jt("action",z)}},{key:"defaultTarget",value:function(z){var Z=Jt("target",z);if(Z)return document.querySelector(Z)}},{key:"defaultText",value:function(z){return Jt("text",z)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(z){var Z=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return x(z,Z)}},{key:"cut",value:function(z){return h(z)}},{key:"isSupported",value:function(){var z=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],Z=typeof z=="string"?[z]:z,We=!!document.queryCommandSupported;return Z.forEach(function(Xt){We=We&&!!document.queryCommandSupported(Xt)}),We}}]),C})(s()),gr=Ht}),828:(function(n){var o=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function a(s,c){for(;s&&s.nodeType!==o;){if(typeof s.matches=="function"&&s.matches(c))return s;s=s.parentNode}}n.exports=a}),438:(function(n,o,i){var a=i(828);function s(u,f,p,m,h){var v=l.apply(this,arguments);return u.addEventListener(p,v,h),{destroy:function(){u.removeEventListener(p,v,h)}}}function c(u,f,p,m,h){return typeof u.addEventListener=="function"?s.apply(null,arguments):typeof p=="function"?s.bind(null,document).apply(null,arguments):(typeof u=="string"&&(u=document.querySelectorAll(u)),Array.prototype.map.call(u,function(v){return s(v,f,p,m,h)}))}function l(u,f,p,m){return function(h){h.delegateTarget=a(h.target,f),h.delegateTarget&&m.call(u,h)}}n.exports=c}),879:(function(n,o){o.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},o.nodeList=function(i){var a=Object.prototype.toString.call(i);return i!==void 0&&(a==="[object NodeList]"||a==="[object HTMLCollection]")&&"length"in i&&(i.length===0||o.node(i[0]))},o.string=function(i){return typeof i=="string"||i instanceof String},o.fn=function(i){var a=Object.prototype.toString.call(i);return a==="[object Function]"}}),370:(function(n,o,i){var a=i(879),s=i(438);function c(p,m,h){if(!p&&!m&&!h)throw new Error("Missing required arguments");if(!a.string(m))throw new TypeError("Second argument must be a String");if(!a.fn(h))throw new TypeError("Third argument must be a Function");if(a.node(p))return l(p,m,h);if(a.nodeList(p))return u(p,m,h);if(a.string(p))return f(p,m,h);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function l(p,m,h){return p.addEventListener(m,h),{destroy:function(){p.removeEventListener(m,h)}}}function u(p,m,h){return Array.prototype.forEach.call(p,function(v){v.addEventListener(m,h)}),{destroy:function(){Array.prototype.forEach.call(p,function(v){v.removeEventListener(m,h)})}}}function f(p,m,h){return s(document.body,p,m,h)}n.exports=c}),817:(function(n){function o(i){var a;if(i.nodeName==="SELECT")i.focus(),a=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var s=i.hasAttribute("readonly");s||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),s||i.removeAttribute("readonly"),a=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var c=window.getSelection(),l=document.createRange();l.selectNodeContents(i),c.removeAllRanges(),c.addRange(l),a=c.toString()}return a}n.exports=o}),279:(function(n){function o(){}o.prototype={on:function(i,a,s){var c=this.e||(this.e={});return(c[i]||(c[i]=[])).push({fn:a,ctx:s}),this},once:function(i,a,s){var c=this;function l(){c.off(i,l),a.apply(s,arguments)}return l._=a,this.on(i,l,s)},emit:function(i){var a=[].slice.call(arguments,1),s=((this.e||(this.e={}))[i]||[]).slice(),c=0,l=s.length;for(c;c0&&i[i.length-1])&&(l[0]===6||l[0]===2)){r=0;continue}if(l[0]===3&&(!i||l[1]>i[0]&&l[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function te(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var n=r.call(e),o,i=[],a;try{for(;(t===void 0||t-- >0)&&!(o=n.next()).done;)i.push(o.value)}catch(s){a={error:s}}finally{try{o&&!o.done&&(r=n.return)&&r.call(n)}finally{if(a)throw a.error}}return i}function ae(e,t,r){if(r||arguments.length===2)for(var n=0,o=t.length,i;n1||c(m,v)})},h&&(o[m]=h(o[m])))}function c(m,h){try{l(n[m](h))}catch(v){p(i[0][3],v)}}function l(m){m.value instanceof $t?Promise.resolve(m.value.v).then(u,f):p(i[0][2],m)}function u(m){c("next",m)}function f(m){c("throw",m)}function p(m,h){m(h),i.shift(),i.length&&c(i[0][0],i[0][1])}}function Yo(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof $e=="function"?$e(e):e[Symbol.iterator](),r={},n("next"),n("throw"),n("return"),r[Symbol.asyncIterator]=function(){return this},r);function n(i){r[i]=e[i]&&function(a){return new Promise(function(s,c){a=e[i](a),o(s,c,a.done,a.value)})}}function o(i,a,s,c){Promise.resolve(c).then(function(l){i({value:l,done:s})},a)}}function U(e){return typeof e=="function"}function Zt(e){var t=function(n){Error.call(n),n.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var Kr=Zt(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: +`+r.map(function(n,o){return o+1+") "+n.toString()}).join(` + `):"",this.name="UnsubscriptionError",this.errors=r}});function pt(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var rt=(function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,n,o,i;if(!this.closed){this.closed=!0;var a=this._parentage;if(a)if(this._parentage=null,Array.isArray(a))try{for(var s=$e(a),c=s.next();!c.done;c=s.next()){var l=c.value;l.remove(this)}}catch(v){t={error:v}}finally{try{c&&!c.done&&(r=s.return)&&r.call(s)}finally{if(t)throw t.error}}else a.remove(this);var u=this.initialTeardown;if(U(u))try{u()}catch(v){i=v instanceof Kr?v.errors:[v]}var f=this._finalizers;if(f){this._finalizers=null;try{for(var p=$e(f),m=p.next();!m.done;m=p.next()){var h=m.value;try{Jo(h)}catch(v){i=i!=null?i:[],v instanceof Kr?i=ae(ae([],te(i)),te(v.errors)):i.push(v)}}}catch(v){n={error:v}}finally{try{m&&!m.done&&(o=p.return)&&o.call(p)}finally{if(n)throw n.error}}}if(i)throw new Kr(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)Jo(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&pt(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&pt(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=(function(){var t=new e;return t.closed=!0,t})(),e})();var Dn=rt.EMPTY;function Br(e){return e instanceof rt||e&&"closed"in e&&U(e.remove)&&U(e.add)&&U(e.unsubscribe)}function Jo(e){U(e)?e():e.unsubscribe()}var Je={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var Qt={setTimeout:function(e,t){for(var r=[],n=2;n0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var n=this,o=this,i=o.hasError,a=o.isStopped,s=o.observers;return i||a?Dn:(this.currentObservers=null,s.push(r),new rt(function(){n.currentObservers=null,pt(s,r)}))},t.prototype._checkFinalizedStatuses=function(r){var n=this,o=n.hasError,i=n.thrownError,a=n.isStopped;o?r.error(i):a&&r.complete()},t.prototype.asObservable=function(){var r=new F;return r.source=this,r},t.create=function(r,n){return new oi(r,n)},t})(F);var oi=(function(e){pe(t,e);function t(r,n){var o=e.call(this)||this;return o.destination=r,o.source=n,o}return t.prototype.next=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.next)===null||o===void 0||o.call(n,r)},t.prototype.error=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.error)===null||o===void 0||o.call(n,r)},t.prototype.complete=function(){var r,n;(n=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||n===void 0||n.call(r)},t.prototype._subscribe=function(r){var n,o;return(o=(n=this.source)===null||n===void 0?void 0:n.subscribe(r))!==null&&o!==void 0?o:Dn},t})(I);var Kn=(function(e){pe(t,e);function t(r){var n=e.call(this)||this;return n._value=r,n}return Object.defineProperty(t.prototype,"value",{get:function(){return this.getValue()},enumerable:!1,configurable:!0}),t.prototype._subscribe=function(r){var n=e.prototype._subscribe.call(this,r);return!n.closed&&r.next(this._value),n},t.prototype.getValue=function(){var r=this,n=r.hasError,o=r.thrownError,i=r._value;if(n)throw o;return this._throwIfClosed(),i},t.prototype.next=function(r){e.prototype.next.call(this,this._value=r)},t})(I);var Er={now:function(){return(Er.delegate||Date).now()},delegate:void 0};var Tr=(function(e){pe(t,e);function t(r,n,o){r===void 0&&(r=1/0),n===void 0&&(n=1/0),o===void 0&&(o=Er);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=n,i._timestampProvider=o,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=n===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,n),i}return t.prototype.next=function(r){var n=this,o=n.isStopped,i=n._buffer,a=n._infiniteTimeWindow,s=n._timestampProvider,c=n._windowTime;o||(i.push(r),!a&&i.push(s.now()+c)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var n=this._innerSubscribe(r),o=this,i=o._infiniteTimeWindow,a=o._buffer,s=a.slice(),c=0;c0?e.prototype.schedule.call(this,r,n):(this.delay=n,this.state=r,this.scheduler.flush(this),this)},t.prototype.execute=function(r,n){return n>0||this.closed?e.prototype.execute.call(this,r,n):this._execute(r,n)},t.prototype.requestAsyncId=function(r,n,o){return o===void 0&&(o=0),o!=null&&o>0||o==null&&this.delay>0?e.prototype.requestAsyncId.call(this,r,n,o):(r.flush(this),0)},t})(nr);var si=(function(e){pe(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t})(or);var Yn=new si(ai);var ci=(function(e){pe(t,e);function t(r,n){var o=e.call(this,r,n)||this;return o.scheduler=r,o.work=n,o}return t.prototype.requestAsyncId=function(r,n,o){return o===void 0&&(o=0),o!==null&&o>0?e.prototype.requestAsyncId.call(this,r,n,o):(r.actions.push(this),r._scheduled||(r._scheduled=rr.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,n,o){var i;if(o===void 0&&(o=0),o!=null?o>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,n,o);var a=r.actions;n!=null&&n===r._scheduled&&((i=a[a.length-1])===null||i===void 0?void 0:i.id)!==n&&(rr.cancelAnimationFrame(n),r._scheduled=void 0)},t})(nr);var li=(function(e){pe(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var n;r?n=r.id:(n=this._scheduled,this._scheduled=void 0);var o=this.actions,i;r=r||o.shift();do if(i=r.execute(r.state,r.delay))break;while((r=o[0])&&r.id===n&&o.shift());if(this._active=!1,i){for(;(r=o[0])&&r.id===n&&o.shift();)r.unsubscribe();throw i}},t})(or);var je=new li(ci);var w=new F(function(e){return e.complete()});function Jr(e){return e&&U(e.schedule)}function Jn(e){return e[e.length-1]}function xt(e){return U(Jn(e))?e.pop():void 0}function Ke(e){return Jr(Jn(e))?e.pop():void 0}function Xr(e,t){return typeof Jn(e)=="number"?e.pop():t}var ir=(function(e){return e&&typeof e.length=="number"&&typeof e!="function"});function Zr(e){return U(e==null?void 0:e.then)}function Qr(e){return U(e[tr])}function en(e){return Symbol.asyncIterator&&U(e==null?void 0:e[Symbol.asyncIterator])}function tn(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function tl(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var rn=tl();function nn(e){return U(e==null?void 0:e[rn])}function on(e){return Go(this,arguments,function(){var r,n,o,i;return qr(this,function(a){switch(a.label){case 0:r=e.getReader(),a.label=1;case 1:a.trys.push([1,,9,10]),a.label=2;case 2:return[4,$t(r.read())];case 3:return n=a.sent(),o=n.value,i=n.done,i?[4,$t(void 0)]:[3,5];case 4:return[2,a.sent()];case 5:return[4,$t(o)];case 6:return[4,a.sent()];case 7:return a.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function an(e){return U(e==null?void 0:e.getReader)}function K(e){if(e instanceof F)return e;if(e!=null){if(Qr(e))return rl(e);if(ir(e))return nl(e);if(Zr(e))return ol(e);if(en(e))return ui(e);if(nn(e))return il(e);if(an(e))return al(e)}throw tn(e)}function rl(e){return new F(function(t){var r=e[tr]();if(U(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function nl(e){return new F(function(t){for(var r=0;r=2;return function(n){return n.pipe(e?O(function(o,i){return e(o,i,n)}):Le,Me(1),r?ot(t):Li(function(){return new ar}))}}function to(e){return e<=0?function(){return w}:T(function(t,r){var n=[];t.subscribe(E(r,function(o){n.push(o),e=2,!0))}function xe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new I}:t,n=e.resetOnError,o=n===void 0?!0:n,i=e.resetOnComplete,a=i===void 0?!0:i,s=e.resetOnRefCountZero,c=s===void 0?!0:s;return function(l){var u,f,p,m=0,h=!1,v=!1,_=function(){f==null||f.unsubscribe(),f=void 0},S=function(){_(),u=p=void 0,h=v=!1},x=function(){var g=u;S(),g==null||g.unsubscribe()};return T(function(g,ee){m++,!v&&!h&&_();var ue=p=p!=null?p:r();ee.add(function(){m--,m===0&&!v&&!h&&(f=ro(x,c))}),ue.subscribe(ee),!u&&m>0&&(u=new ft({next:function(L){return ue.next(L)},error:function(L){v=!0,_(),f=ro(S,o,L),ue.error(L)},complete:function(){h=!0,_(),f=ro(S,a),ue.complete()}}),K(g).subscribe(u))})(l)}}function ro(e,t){for(var r=[],n=2;ne.next(document)),e}function $(e,t=document){return Array.from(t.querySelectorAll(e))}function Y(e,t=document){let r=we(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function we(e,t=document){return t.querySelector(e)||void 0}function Et(){var e,t,r,n;return(n=(r=(t=(e=document.activeElement)==null?void 0:e.shadowRoot)==null?void 0:t.activeElement)!=null?r:document.activeElement)!=null?n:void 0}var Tl=R(b(document.body,"focusin"),b(document.body,"focusout")).pipe(Ge(1),J(void 0),d(()=>Et()||document.body),re(1));function cr(e){return Tl.pipe(d(t=>e.contains(t)),ce())}function Dt(e,t){let{matches:r}=matchMedia("(hover)");return j(()=>(r?R(b(e,"mouseenter").pipe(d(()=>!0)),b(e,"mouseleave").pipe(d(()=>!1))):R(b(e,"touchstart").pipe(d(()=>!0)),b(e,"touchend").pipe(d(()=>!1)),b(e,"touchcancel").pipe(d(()=>!1)))).pipe(t?Or(o=>Ve(+!o*t)):Le,J(!0,e.matches(":hover"))))}function Ci(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)Ci(e,r)}function A(e,t,...r){let n=document.createElement(e);if(t)for(let o of Object.keys(t))typeof t[o]!="undefined"&&(typeof t[o]!="boolean"?n.setAttribute(o,t[o]):n.setAttribute(o,""));for(let o of r)Ci(n,o);return n}function Hi(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function it(e){let t=A("script",{src:e});return j(()=>(document.head.appendChild(t),R(b(t,"load"),b(t,"error").pipe(y(()=>cn(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(d(()=>{}),q(()=>document.head.removeChild(t)),Me(1))))}function $i(e){let t=A("link",{rel:"stylesheet",href:e});return document.head.appendChild(t),R(b(t,"load"),b(t,"error").pipe(y(()=>cn(()=>new ReferenceError(`Invalid styles: ${e}`))))).pipe(d(()=>{}),Me(1))}var Pi=new I,Sl=j(()=>typeof ResizeObserver=="undefined"?it("https://unpkg.com/resize-observer-polyfill"):D(void 0)).pipe(d(()=>new ResizeObserver(e=>e.forEach(t=>Pi.next(t)))),y(e=>R(Be,D(e)).pipe(q(()=>e.disconnect()))),re(1));function Ae(e){return{width:e.offsetWidth,height:e.offsetHeight}}function Re(e){let t=e;for(;t.clientWidth===0&&t.parentElement;)t=t.parentElement;return Sl.pipe(P(r=>r.observe(t)),y(r=>Pi.pipe(O(n=>n.target===t),q(()=>r.unobserve(t)))),d(()=>Ae(e)),J(Ae(e)))}function Ar(e){return{width:e.scrollWidth,height:e.scrollHeight}}function Ii(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}function Ri(e){let t=[],r=e.parentElement;for(;r;)(e.clientWidth>r.clientWidth||e.clientHeight>r.clientHeight)&&t.push(r),r=(e=r).parentElement;return t.length===0&&t.push(document.documentElement),t}function Tt(e){return{x:e.offsetLeft,y:e.offsetTop}}function ji(e){let t=e.getBoundingClientRect();return{x:t.x+window.scrollX,y:t.y+window.scrollY}}function Fi(e){return R(b(window,"load"),b(window,"resize")).pipe(Xe(0,je),d(()=>Tt(e)),J(Tt(e)))}function dn(e){return{x:e.scrollLeft,y:e.scrollTop}}function Wt(e){return R(b(e,"scroll"),b(window,"scroll"),b(window,"resize")).pipe(Xe(0,je),d(()=>dn(e)),J(dn(e)))}var Ui=new I,Ol=j(()=>D(new IntersectionObserver(e=>{for(let t of e)Ui.next(t)},{threshold:0}))).pipe(y(e=>R(Be,D(e)).pipe(q(()=>e.disconnect()))),re(1));function St(e){return Ol.pipe(P(t=>t.observe(e)),y(t=>Ui.pipe(O(({target:r})=>r===e),q(()=>t.unobserve(e)),d(({isIntersecting:r})=>r))))}var Ll=Object.create,ba=Object.defineProperty,Ml=Object.getOwnPropertyDescriptor,kl=Object.getOwnPropertyNames,Al=Object.getPrototypeOf,Cl=Object.prototype.hasOwnProperty,Hl=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),$l=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of kl(t))!Cl.call(e,o)&&o!==r&&ba(e,o,{get:()=>t[o],enumerable:!(n=Ml(t,o))||n.enumerable});return e},Pl=(e,t,r)=>(r=e!=null?Ll(Al(e)):{},$l(t||!e||!e.__esModule?ba(r,"default",{value:e,enumerable:!0}):r,e)),Il=Hl((e,t)=>{var r="Expected a function",n=NaN,o="[object Symbol]",i=/^\s+|\s+$/g,a=/^[-+]0x[0-9a-f]+$/i,s=/^0b[01]+$/i,c=/^0o[0-7]+$/i,l=parseInt,u=typeof global=="object"&&global&&global.Object===Object&&global,f=typeof self=="object"&&self&&self.Object===Object&&self,p=u||f||Function("return this")(),m=Object.prototype,h=m.toString,v=Math.max,_=Math.min,S=function(){return p.Date.now()};function x(M,W,ie){var ne,de,Yt,yt,De,lt,tt=0,Jt=!1,Ht=!1,gr=!0;if(typeof M!="function")throw new TypeError(r);W=L(W)||0,g(ie)&&(Jt=!!ie.leading,Ht="maxWait"in ie,Yt=Ht?v(L(ie.maxWait)||0,W):Yt,gr="trailing"in ie?!!ie.trailing:gr);function G(Se){var _t=ne,yr=de;return ne=de=void 0,tt=Se,yt=M.apply(yr,_t),yt}function H(Se){return tt=Se,De=setTimeout(z,W),Jt?G(Se):yt}function C(Se){var _t=Se-lt,yr=Se-tt,Wo=W-_t;return Ht?_(Wo,Yt-yr):Wo}function V(Se){var _t=Se-lt,yr=Se-tt;return lt===void 0||_t>=W||_t<0||Ht&&yr>=Yt}function z(){var Se=S();if(V(Se))return Z(Se);De=setTimeout(z,C(Se))}function Z(Se){return De=void 0,gr&&ne?G(Se):(ne=de=void 0,yt)}function We(){De!==void 0&&clearTimeout(De),tt=0,ne=lt=de=De=void 0}function Xt(){return De===void 0?yt:Z(S())}function Vr(){var Se=S(),_t=V(Se);if(ne=arguments,de=this,lt=Se,_t){if(De===void 0)return H(lt);if(Ht)return De=setTimeout(z,W),G(lt)}return De===void 0&&(De=setTimeout(z,W)),yt}return Vr.cancel=We,Vr.flush=Xt,Vr}function g(M){var W=typeof M;return!!M&&(W=="object"||W=="function")}function ee(M){return!!M&&typeof M=="object"}function ue(M){return typeof M=="symbol"||ee(M)&&h.call(M)==o}function L(M){if(typeof M=="number")return M;if(ue(M))return n;if(g(M)){var W=typeof M.valueOf=="function"?M.valueOf():M;M=g(W)?W+"":W}if(typeof M!="string")return M===0?M:+M;M=M.replace(i,"");var ie=s.test(M);return ie||c.test(M)?l(M.slice(2),ie?2:8):a.test(M)?n:+M}t.exports=x}),Ln,B,ga,ya,Lt,Ni,_a,xa,oo,bn,Cr,wa,ho,co,lo,Rl,xn={},wn=[],jl=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,Rr=Array.isArray;function dt(e,t){for(var r in t)e[r]=t[r];return e}function vo(e){e&&e.parentNode&&e.parentNode.removeChild(e)}function kt(e,t,r){var n,o,i,a={};for(i in t)i=="key"?n=t[i]:i=="ref"?o=t[i]:a[i]=t[i];if(arguments.length>2&&(a.children=arguments.length>3?Ln.call(arguments,2):r),typeof e=="function"&&e.defaultProps!=null)for(i in e.defaultProps)a[i]===void 0&&(a[i]=e.defaultProps[i]);return gn(e,a,n,o,null)}function gn(e,t,r,n,o){var i={type:e,props:t,key:r,ref:n,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:o!=null?o:++ga,__i:-1,__u:0};return o==null&&B.vnode!=null&&B.vnode(i),i}function ze(e){return e.children}function ct(e,t){this.props=e,this.context=t}function ur(e,t){if(t==null)return e.__?ur(e.__,e.__i+1):null;for(var r;tt&&Lt.sort(xa),e=Lt.shift(),t=Lt.length,Fl(e)}finally{Lt.length=En.__r=0}}function Ta(e,t,r,n,o,i,a,s,c,l,u){var f,p,m,h,v,_,S,x=n&&n.__k||wn,g=t.length;for(c=Ul(r,t,x,c,g),f=0;f0?a=e.__k[i]=gn(a.type,a.props,a.key,a.ref?a.ref:null,a.__v):e.__k[i]=a,c=i+p,a.__=e,a.__b=e.__b+1,s=null,(l=a.__i=Nl(a,r,c,f))!=-1&&(f--,(s=r[l])&&(s.__u|=2)),s==null||s.__v==null?(l==-1&&(o>u?p--:oc?p--:p++,a.__u|=4))):e.__k[i]=null;if(f)for(i=0;i(u?1:0)){for(o=r-1,i=r+1;o>=0||i=0?o--:i++])!=null&&!(2&l.__u)&&s==l.key&&c==l.type)return a}return-1}function Wi(e,t,r){t[0]=="-"?e.setProperty(t,r!=null?r:""):e[t]=r==null?"":typeof r!="number"||jl.test(t)?r:r+"px"}function hn(e,t,r,n,o){var i,a;e:if(t=="style")if(typeof r=="string")e.style.cssText=r;else{if(typeof n=="string"&&(e.style.cssText=n=""),n)for(t in n)r&&t in r||Wi(e.style,t,"");if(r)for(t in r)n&&r[t]==n[t]||Wi(e.style,t,r[t])}else if(t[0]=="o"&&t[1]=="n")i=t!=(t=t.replace(wa,"$1")),a=t.toLowerCase(),t=a in e||t=="onFocusOut"||t=="onFocusIn"?a.slice(2):t.slice(2),e.l||(e.l={}),e.l[t+i]=r,r?n?r[Cr]=n[Cr]:(r[Cr]=ho,e.addEventListener(t,i?lo:co,i)):e.removeEventListener(t,i?lo:co,i);else{if(o=="http://www.w3.org/2000/svg")t=t.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(t!="width"&&t!="height"&&t!="href"&&t!="list"&&t!="form"&&t!="tabIndex"&&t!="download"&&t!="rowSpan"&&t!="colSpan"&&t!="role"&&t!="popover"&&t in e)try{e[t]=r!=null?r:"";break e}catch(s){}typeof r=="function"||(r==null||r===!1&&t[4]!="-"?e.removeAttribute(t):e.setAttribute(t,t=="popover"&&r==1?"":r))}}function Vi(e){return function(t){if(this.l){var r=this.l[t.type+e];if(t[bn]==null)t[bn]=ho++;else if(t[bn]0?e:Rr(e)?e.map(Ma):e.constructor!==void 0?null:dt({},e)}function Dl(e,t,r,n,o,i,a,s,c){var l,u,f,p,m,h,v,_=r.props||xn,S=t.props,x=t.type;if(x=="svg"?o="http://www.w3.org/2000/svg":x=="math"?o="http://www.w3.org/1998/Math/MathML":o||(o="http://www.w3.org/1999/xhtml"),i!=null){for(l=0;l=r.__.length&&r.__.push({}),r.__[e]}function Tn(e){return Ir=1,zl($a,e)}function zl(e,t,r){var n=yo(Pr++,2);if(n.t=e,!n.__c&&(n.__=[r?r(t):$a(void 0,t),function(s){var c=n.__N?n.__N[0]:n.__[0],l=n.t(c,s);c!==l&&(n.__N=[l,n.__[1]],n.__c.setState({}))}],n.__c=ge,!ge.__f)){var o=function(s,c,l){if(!n.__c.__H)return!0;var u=!1,f=n.__c.props!==s;if(n.__c.__H.__.some(function(m){if(m.__N){u=!0;var h=m.__[0];m.__=m.__N,m.__N=void 0,h!==m.__[0]&&(f=!0)}}),i){var p=i.call(this,s,c,l);return u?p||f:p}return!u||f};ge.__f=!0;var i=ge.shouldComponentUpdate,a=ge.componentWillUpdate;ge.componentWillUpdate=function(s,c,l){if(this.__e){var u=i;i=void 0,o(s,c,l),i=u}a&&a.call(this,s,c,l)},ge.shouldComponentUpdate=o}return n.__N||n.__}function ht(e,t){var r=yo(Pr++,3);!Ee.__s&&Ha(r.__H,t)&&(r.__=e,r.u=t,ge.__H.__h.push(r))}function zt(e){return Ir=5,fr(function(){return{current:e}},[])}function fr(e,t){var r=yo(Pr++,7);return Ha(r.__H,t)&&(r.__=e(),r.__H=t,r.__h=e),r.__}function ql(e,t){return Ir=8,fr(function(){return e},t)}function Xi(){for(var e;e=Aa.shift();){var t=e.__H;if(e.__P&&t)try{t.__h.some(uo),t.__h.some(Ca),t.__h=[]}catch(r){t.__h=[],Ee.__e(r,e.__v)}}}Ee.__b=function(e){ge=null,qi&&qi(e)},Ee.__=function(e,t){e&&t.__k&&t.__k.__m&&(e.__m=t.__k.__m),Ji&&Ji(e,t)},Ee.__r=function(e){Ki&&Ki(e),Pr=0;var t=(ge=e.__c).__H;t&&(io===ge?(t.__h=[],ge.__h=[],t.__.some(function(r){r.__N&&(r.__=r.__N),r.u=r.__N=void 0})):(t.__h.length&&Xi(),Pr=0)),io=ge},Ee.diffed=function(e){Bi&&Bi(e);var t=e.__c;t&&t.__H&&(t.__H.__h.length&&(Aa.push(t)!==1&&zi===Ee.requestAnimationFrame||((zi=Ee.requestAnimationFrame)||Kl)(Xi)),t.__H.__.some(function(r){r.u&&(r.__H=r.u,r.u=void 0)})),io=ge=null},Ee.__c=function(e,t){t.some(function(r){try{r.__h.some(uo),r.__h=r.__h.filter(function(n){return!n.__||Ca(n)})}catch(n){t.some(function(o){o.__h&&(o.__h=[])}),t=[],Ee.__e(n,r.__v)}}),Gi&&Gi(e,t)},Ee.unmount=function(e){Yi&&Yi(e);var t,r=e.__c;r&&r.__H&&(r.__H.__.some(function(n){try{uo(n)}catch(o){t=o}}),r.__H=void 0,t&&Ee.__e(t,r.__v))};var Zi=typeof requestAnimationFrame=="function";function Kl(e){var t,r=function(){clearTimeout(n),Zi&&cancelAnimationFrame(t),setTimeout(e)},n=setTimeout(r,35);Zi&&(t=requestAnimationFrame(r))}function uo(e){var t=ge,r=e.__c;typeof r=="function"&&(e.__c=void 0,r()),ge=t}function Ca(e){var t=ge;e.__c=e.__(),ge=t}function Ha(e,t){return!e||e.length!==t.length||t.some(function(r,n){return r!==e[n]})}function $a(e,t){return typeof t=="function"?t(e):t}function Bl(e,t){for(var r in t)e[r]=t[r];return e}function Qi(e,t){for(var r in e)if(r!=="__source"&&!(r in t))return!0;for(var n in t)if(n!=="__source"&&e[n]!==t[n])return!0;return!1}function ea(e,t){this.props=e,this.context=t}(ea.prototype=new ct).isPureReactComponent=!0,ea.prototype.shouldComponentUpdate=function(e,t){return Qi(this.props,e)||Qi(this.state,t)};var ta=B.__b;B.__b=function(e){e.type&&e.type.__f&&e.ref&&(e.props.ref=e.ref,e.ref=null),ta&&ta(e)};var kw=typeof Symbol<"u"&&Symbol.for&&Symbol.for("react.forward_ref")||3911,Gl=B.__e;B.__e=function(e,t,r,n){if(e.then){for(var o,i=t;i=i.__;)if((o=i.__c)&&o.__c)return t.__e==null&&(t.__e=r.__e,t.__k=r.__k||[]),o.__c(e,t)}Gl(e,t,r,n)};var ra=B.unmount;function Pa(e,t,r){return e&&(e.__c&&e.__c.__H&&(e.__c.__H.__.forEach(function(n){typeof n.__c=="function"&&n.__c()}),e.__c.__H=null),(e=Bl({},e)).__c!=null&&(e.__c.__P===r&&(e.__c.__P=t),e.__c.__e=!0,e.__c=null),e.__k=e.__k&&e.__k.map(function(n){return Pa(n,t,r)})),e}function Ia(e,t,r){return e&&r&&(e.__v=null,e.__k=e.__k&&e.__k.map(function(n){return Ia(n,t,r)}),e.__c&&e.__c.__P===t&&(e.__e&&r.appendChild(e.__e),e.__c.__e=!0,e.__c.__P=r)),e}function ao(){this.__u=0,this.o=null,this.__b=null}function Ra(e){var t=e.__&&e.__.__c;return t&&t.__a&&t.__a(e)}function vn(){this.i=null,this.l=null}B.unmount=function(e){var t=e.__c;t&&(t.__z=!0),t&&t.__R&&t.__R(),t&&32&e.__u&&(e.type=null),ra&&ra(e)},(ao.prototype=new ct).__c=function(e,t){var r=t.__c,n=this;n.o==null&&(n.o=[]),n.o.push(r);var o=Ra(n.__v),i=!1,a=function(){i||n.__z||(i=!0,r.__R=null,o?o(c):c())};r.__R=a;var s=r.__P;r.__P=null;var c=function(){if(!--n.__u){if(n.state.__a){var l=n.state.__a;n.__v.__k[0]=Ia(l,l.__c.__P,l.__c.__O)}var u;for(n.setState({__a:n.__b=null});u=n.o.pop();)u.__P=s,u.forceUpdate()}};n.__u++||32&t.__u||n.setState({__a:n.__b=n.__v.__k[0]}),e.then(a,a)},ao.prototype.componentWillUnmount=function(){this.o=[]},ao.prototype.render=function(e,t){if(this.__b){if(this.__v.__k){var r=document.createElement("div"),n=this.__v.__k[0].__c;this.__v.__k[0]=Pa(this.__b,r,n.__O=n.__P)}this.__b=null}var o=t.__a&&kt(ze,null,e.fallback);return o&&(o.__u&=-33),[kt(ze,null,t.__a?null:e.children),o]};var na=function(e,t,r){if(++r[1]===r[0]&&e.l.delete(t),e.props.revealOrder&&(e.props.revealOrder[0]!=="t"||!e.l.size))for(r=e.i;r;){for(;r.length>3;)r.pop()();if(r[1]Object.freeze({get current(){return t.current}}),[])}var ru=typeof globalThis<"u"&&typeof navigator<"u"&&typeof document<"u";function nu(e,...t){var r;(r=e==null?void 0:e.addEventListener)==null||r.call(e,...t)}function ou(e,...t){var r;(r=e==null?void 0:e.removeEventListener)==null||r.call(e,...t)}var iu=(e,t)=>Object.hasOwn(e,t),au=()=>!0,su=()=>!1;function cu(e=!1){let t=zt(e),r=ql(()=>t.current,[]);return ht(()=>(t.current=!0,()=>{t.current=!1}),[]),r}function lu(e,...t){let r=cu(),n=Fa(t[1]),o=fr(()=>function(...i){r()&&(typeof n.current=="function"?n.current.apply(this,i):typeof n.current.handleEvent=="function"&&n.current.handleEvent.apply(this,i))},[]);ht(()=>{let i=uu(e)?e.current:e;if(!i)return;let a=t.slice(2);return nu(i,t[0],o,...a),()=>{ou(i,t[0],o,...a)}},[e,t[0]])}function uu(e){return e!==null&&typeof e=="object"&&iu(e,"current")}var pu=e=>typeof e=="function"?e:typeof e=="string"?t=>t.key===e:e?au:su,fu=ru?globalThis:null;function Ua(e,t,r=[],n={}){let{event:o="keydown",target:i=fu,eventOptions:a}=n,s=Fa(t),c=fr(()=>{let l=pu(e);return function(u){l(u)&&s.current.call(this,u)}},r);lu(i,o,c,a)}function Na(e){var t,r,n="";if(typeof e=="string"||typeof e=="number")n+=e;else if(typeof e=="object")if(Array.isArray(e)){var o=e.length;for(t=0;t1)Mt--;else{for(var e,t=!1;Hr!==void 0;){var r=Hr;for(Hr=void 0,po++;r!==void 0;){var n=r.o;if(r.o=void 0,r.f&=-3,!(8&r.f)&&Va(r))try{r.c()}catch(o){t||(e=o,t=!0)}r=n}}if(po=0,Mt--,t)throw e}}function hu(e){if(Mt>0)return e();Mt++;try{return e()}finally{Mn()}}var le=void 0;function Da(e){var t=le;le=void 0;try{return e()}finally{le=t}}var Hr=void 0,Mt=0,po=0,Sn=0;function Wa(e){if(le!==void 0){var t=e.n;if(t===void 0||t.t!==le)return t={i:0,S:e,p:le.s,n:void 0,t:le,e:void 0,x:void 0,r:t},le.s!==void 0&&(le.s.n=t),le.s=t,e.n=t,32&le.f&&e.S(t),t;if(t.i===-1)return t.i=0,t.n!==void 0&&(t.n.p=t.p,t.p!==void 0&&(t.p.n=t.n),t.p=le.s,t.n=void 0,le.s.n=t,le.s=t),t}}function Ce(e,t){this.v=e,this.i=0,this.n=void 0,this.t=void 0,this.W=t==null?void 0:t.watched,this.Z=t==null?void 0:t.unwatched,this.name=t==null?void 0:t.name}Ce.prototype.brand=du;Ce.prototype.h=function(){return!0};Ce.prototype.S=function(e){var t=this,r=this.t;r!==e&&e.e===void 0&&(e.x=r,this.t=e,r!==void 0?r.e=e:Da(function(){var n;(n=t.W)==null||n.call(t)}))};Ce.prototype.U=function(e){var t=this;if(this.t!==void 0){var r=e.e,n=e.x;r!==void 0&&(r.x=n,e.e=void 0),n!==void 0&&(n.e=r,e.x=void 0),e===this.t&&(this.t=n,n===void 0&&Da(function(){var o;(o=t.Z)==null||o.call(t)}))}};Ce.prototype.subscribe=function(e){var t=this;return Kt(function(){var r=t.value,n=le;le=void 0;try{e(r)}finally{le=n}},{name:"sub"})};Ce.prototype.valueOf=function(){return this.value};Ce.prototype.toString=function(){return this.value+""};Ce.prototype.toJSON=function(){return this.value};Ce.prototype.peek=function(){var e=le;le=void 0;try{return this.value}finally{le=e}};Object.defineProperty(Ce.prototype,"value",{get:function(){var e=Wa(this);return e!==void 0&&(e.i=this.i),this.v},set:function(e){if(e!==this.v){if(po>100)throw new Error("Cycle detected");this.v=e,this.i++,Sn++,Mt++;try{for(var t=this.t;t!==void 0;t=t.x)t.t.N()}finally{Mn()}}}});function At(e,t){return new Ce(e,t)}function Va(e){for(var t=e.s;t!==void 0;t=t.n)if(t.S.i!==t.i||!t.S.h()||t.S.i!==t.i)return!0;return!1}function za(e){for(var t=e.s;t!==void 0;t=t.n){var r=t.S.n;if(r!==void 0&&(t.r=r),t.S.n=t,t.i=-1,t.n===void 0){e.s=t;break}}}function qa(e){for(var t=e.s,r=void 0;t!==void 0;){var n=t.p;t.i===-1?(t.S.U(t),n!==void 0&&(n.n=t.n),t.n!==void 0&&(t.n.p=n)):r=t,t.S.n=t.r,t.r!==void 0&&(t.r=void 0),t=n}e.s=r}function Bt(e,t){Ce.call(this,void 0),this.x=e,this.s=void 0,this.g=Sn-1,this.f=4,this.W=t==null?void 0:t.watched,this.Z=t==null?void 0:t.unwatched,this.name=t==null?void 0:t.name}Bt.prototype=new Ce;Bt.prototype.h=function(){if(this.f&=-3,1&this.f)return!1;if((36&this.f)==32||(this.f&=-5,this.g===Sn))return!0;if(this.g=Sn,this.f|=1,this.i>0&&!Va(this))return this.f&=-2,!0;var e=le;try{za(this),le=this;var t=this.x();(16&this.f||this.v!==t||this.i===0)&&(this.v=t,this.f&=-17,this.i++)}catch(r){this.v=r,this.f|=16,this.i++}return le=e,qa(this),this.f&=-2,!0};Bt.prototype.S=function(e){if(this.t===void 0){this.f|=36;for(var t=this.s;t!==void 0;t=t.n)t.S.S(t)}Ce.prototype.S.call(this,e)};Bt.prototype.U=function(e){if(this.t!==void 0&&(Ce.prototype.U.call(this,e),this.t===void 0)){this.f&=-33;for(var t=this.s;t!==void 0;t=t.n)t.S.U(t)}};Bt.prototype.N=function(){if(!(2&this.f)){this.f|=6;for(var e=this.t;e!==void 0;e=e.x)e.t.N()}};Object.defineProperty(Bt.prototype,"value",{get:function(){if(1&this.f)throw new Error("Cycle detected");var e=Wa(this);if(this.h(),e!==void 0&&(e.i=this.i),16&this.f)throw this.v;return this.v}});function ca(e,t){return new Bt(e,t)}function Ka(e){var t=e.u;if(e.u=void 0,typeof t=="function"){Mt++;var r=le;le=void 0;try{t()}catch(n){throw e.f&=-2,e.f|=8,_o(e),n}finally{le=r,Mn()}}}function _o(e){for(var t=e.s;t!==void 0;t=t.n)t.S.U(t);e.x=void 0,e.s=void 0,Ka(e)}function vu(e){if(le!==this)throw new Error("Out-of-order effect");qa(this),le=e,this.f&=-2,8&this.f&&_o(this),Mn()}function mr(e,t){this.x=e,this.u=void 0,this.s=void 0,this.o=void 0,this.f=32,this.name=t==null?void 0:t.name}mr.prototype.c=function(){var e=this.S();try{if(8&this.f||this.x===void 0)return;var t=this.x();typeof t=="function"&&(this.u=t)}finally{e()}};mr.prototype.S=function(){if(1&this.f)throw new Error("Cycle detected");this.f|=1,this.f&=-9,Ka(this),za(this),Mt++;var e=le;return le=this,vu.bind(this,e)};mr.prototype.N=function(){2&this.f||(this.f|=2,this.o=Hr,Hr=this)};mr.prototype.d=function(){this.f|=8,1&this.f||_o(this)};mr.prototype.dispose=function(){this.d()};function Kt(e,t){var r=new mr(e,t);try{r.c()}catch(o){throw r.d(),o}var n=r.d.bind(r);return n[Symbol.dispose]=n,n}var Ba,xo,so,Ga=[];Kt(function(){Ba=this.N})();function dr(e,t){B[e]=t.bind(null,B[e]||function(){})}function On(e){so&&so(),so=e&&e.S()}function Ya(e){var t=this,r=e.data,n=gu(r);n.value=r;var o=fr(function(){for(var s=t,c=t.__v;c=c.__;)if(c.__c){c.__c.__$f|=4;break}var l=ca(function(){var m=n.value.value;return m===0?0:m===!0?"":m||""}),u=ca(function(){return!Array.isArray(l.value)&&!ya(l.value)}),f=Kt(function(){if(this.N=Ja,u.value){var m=l.value;s.__v&&s.__v.__e&&s.__v.__e.nodeType===3&&(s.__v.__e.data=m)}}),p=t.__$u.d;return t.__$u.d=function(){f(),p.call(this)},[u,l]},[]),i=o[0],a=o[1];return i.value?a.peek():a.value}Ya.displayName="ReactiveTextNode";Object.defineProperties(Ce.prototype,{constructor:{configurable:!0,value:void 0},type:{configurable:!0,value:Ya},props:{configurable:!0,get:function(){return{data:this}}},__b:{configurable:!0,value:1}});dr("__b",function(e,t){if(typeof t.type=="function"&&typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent(),typeof t.type=="string"){var r,n=t.props;for(var o in n)if(o!=="children"){var i=n[o];i instanceof Ce&&(r||(t.__np=r={}),r[o]=i,n[o]=i.peek())}}e(t)});dr("__r",function(e,t){if(typeof t.type=="function"&&typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.enterComponent(t),t.type!==ze){On();var r,n=t.__c;n&&(n.__$f&=-2,(r=n.__$u)===void 0&&(n.__$u=r=(function(o){var i;return Kt(function(){i=this}),i.c=function(){n.__$f|=1,n.setState({})},i})())),xo=n,On(r)}e(t)});dr("__e",function(e,t,r,n){typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent(),On(),xo=void 0,e(t,r,n)});dr("diffed",function(e,t){typeof t.type=="function"&&typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent(),On(),xo=void 0;var r;if(typeof t.type=="string"&&(r=t.__e)){var n=t.__np,o=t.props;if(n){var i=r.U;if(i)for(var a in i){var s=i[a];s!==void 0&&!(a in n)&&(s.d(),i[a]=void 0)}else i={},r.U=i;for(var c in n){var l=i[c],u=n[c];l===void 0?(l=bu(r,c,u,o),i[c]=l):l.o(u,o)}}}e(t)});function bu(e,t,r,n){var o=t in e&&e.ownerSVGElement===void 0,i=At(r);return{o:function(a,s){i.value=a,n=s},d:Kt(function(){this.N=Ja;var a=i.value.value;n[t]!==a&&(n[t]=a,o?e[t]=a:a?e.setAttribute(t,a):e.removeAttribute(t))})}}dr("unmount",function(e,t){if(typeof t.type=="string"){var r=t.__e;if(r){var n=r.U;if(n){r.U=void 0;for(var o in n){var i=n[o];i&&i.d()}}}}else{var a=t.__c;if(a){var s=a.__$u;s&&(a.__$u=void 0,s.d())}}e(t)});dr("__h",function(e,t,r,n){(n<3||n===9)&&(t.__$f|=2),e(t,r,n)});ct.prototype.shouldComponentUpdate=function(e,t){var r=this.__$u,n=r&&r.s!==void 0;for(var o in t)return!0;if(this.__f||typeof this.u=="boolean"&&this.u===!0){var i=2&this.__$f;if(!(n||i||4&this.__$f)||1&this.__$f)return!0}else if(!(n||4&this.__$f)||3&this.__$f)return!0;for(var a in e)if(a!=="__source"&&e[a]!==this.props[a])return!0;for(var s in this.props)if(!(s in e))return!0;return!1};function gu(e,t){return Tn(function(){return At(e,t)})[0]}var yu=function(e){queueMicrotask(function(){queueMicrotask(e)})};function _u(){hu(function(){for(var e;e=Ga.shift();)Ba.call(e)})}function Ja(){Ga.push(this)===1&&(B.requestAnimationFrame||yu)(_u)}var fo=[0];for(let e=0;e<32;e++)fo.push(fo[e]|1<>>5]>>>e&1}set(e){this.data[e>>>5]|=1<<(e&31)}forEach(e){let t=this.size&31;for(let r=0;r{var r;return(r=t.tags)==null?void 0:r.length})&&(matchMedia("(max-width: 768px)").matches||Xa())}function Vt(){Qe.value=He(k({},Qe.value),{hideSearch:!Qe.value.hideSearch})}function Xa(){Qe.value=He(k({},Qe.value),{hideFilters:!Qe.value.hideFilters})}function yn(){return Qe.value.selectedItem}function mo(e){Qe.value=He(k({},Qe.value),{selectedItem:e})}function Eu(){var e,t;return(t=(e=pr.value)==null?void 0:e.items)!=null?t:[]}function kn(){return typeof Oe.value.input=="string"?Oe.value.input:""}function Za(e){let t=Qa();e.length&&!t.length?Oe.value=He(k({},Oe.value),{page:void 0,input:e}):!e.length&&t.length?Oe.value=He(k({},Oe.value),{page:void 0,input:{type:"operator",data:{operator:"not",operands:[]}}}):Oe.value=He(k({},Oe.value),{page:void 0,input:e})}function Tu(){typeof st.value.pagination.next<"u"&&(Oe.value=He(k({},Oe.value),{page:st.value.pagination.next}))}function Su(e){let t=Oe.value.filter.input;if("type"in t&&t.type==="operator"){for(let r of t.data.operands)if("type"in r&&r.type==="value"&&typeof r.data.value=="string"&&r.data.value===e)return!0}return!1}function Qa(){let e=Oe.value.filter.input,t=[];if("type"in e&&e.type==="operator")for(let r of e.data.operands)"type"in r&&r.type==="value"&&typeof r.data.value=="string"&&t.push(r.data.value);return t}function Ou(e){let t=Oe.value.filter.input,r=[];if("type"in t&&t.type==="operator")for(let n of t.data.operands)"type"in n&&n.type==="value"&&typeof n.data.value=="string"&&r.push(n.data.value);if(r.includes(e)){let n=r.indexOf(e);n>-1&&r.splice(n,1)}else r.push(e);Oe.value=He(k({},Oe.value),{page:void 0,filter:He(k({},Oe.value.filter),{input:{type:"operator",data:{operator:"and",operands:r.map(n=>({type:"value",data:{field:"tags",value:n}}))}}})}),Za(kn())}function Lu(){return st.value.items}function Mu(){return st.value.total}function ku(){var e;for(let t of(e=st.value.aggregations)!=null?e:[])if(t.type==="term")return t.data.value;return[]}function lr(){return Qe.value.hideSearch}function Au(){return Qe.value.hideFilters}function es(){var e;return(e=ts.value.highlight)!=null?e:!1}var Qe=At({hideSearch:!0,hideFilters:!0,selectedItem:0}),ts=At({}),pr=At(),ua=At(),Oe=At({input:"",filter:{input:{type:"operator",data:{operator:"and",operands:[]}},aggregation:{input:[{type:"term",data:{field:"tags"}}]}}}),st=At({items:[],query:{select:{documents:new la(0),terms:new la(0)},values:[]},pagination:{total:0}});function Cu(e,t,r){for(let n=0;tr&&t(0,o,r,r=i);continue;case 62:e.charCodeAt(r+1)===47?t(2,--o,r,r=i+1):Cu(e,r,n)?t(3,o,r,r=i+1):t(1,o++,r,r=i+1)}i>r&&t(0,o,r,i)}function $u(e,t=0,r=e.length){let n=++t;e:for(let l=0;n{let i=[],a=[],{onElement:s,onText:c=Pu}=typeof r=="function"?{onElement:r}:r,l=0,u=0;return e(t,(f,p,m,h)=>{if(f===0)i[l++]=c(t,m,h),a[u++]={value:null,depth:p};else if(f&1&&(a[u++]={value:$u(t,m,h),depth:p}),f&2)for(let v=0;u>=0;v++){let{value:_,depth:S}=a[--u];if(S>p)continue;let x=i.slice(l-=v,l+v);i[l++]=s(_,x),u++;break}},n,o),i.slice(0,l)}}function Ru(e){return e.replace(/[&<>]/g,t=>{switch(t.charCodeAt(0)){case 38:return"&";case 60:return"<";case 62:return">"}})}function _n(e){return e.replace(/&(amp|[lg]t);/g,t=>{switch(t.charCodeAt(1)){case 97:return"&";case 108:return"<";case 103:return">"}})}function ju(e,t){return{start:e.start+t,end:e.end+t,value:e.value}}function Fu(e,t,r){return e.slice(t,r)}function Uu(e){let{onHighlight:t,onText:r=Fu}=typeof e=="function"?{onHighlight:e}:e;return(n,o,i=0,a=n.length)=>{var l;let s=[],c=(l=o==null?void 0:o.ranges)!=null?l:[];for(let u=0,f=i;ua)break;let m=c[u].end;if(mi&&s.push(r(n,i,p));let{value:h}=c[u];s.push(t(n,{start:p,end:i=m,value:h}))}return it!==null&&typeof t<"u"&&t!==!1&&!(typeof t=="string"&&t.trim().length===0))}function os(e,t){let r=rs(e,{onElement(n,o){return kt(n.tag,n.attrs,...o)},onText(n,o,i){return ns(n,t==null?void 0:t.value.highlight,o,i)}});return N(ze,{children:r})}function qu(e,t){var n;let r=[];for(let o=0,i=0;om.start=s||c.push(He(k({},m),{start:Math.max(m.start,a),end:Math.min(m.end,s)}));let l=rs(e,{onElement(m,h){var _;let v=zu(h);if(v.length!==0)return Vu.has(m.tag)?kt(m.tag,(_=m.attrs)!=null?_:{},...v):m.tag==="li"?N(ze,{children:["\u2013 ",v," "]}):Wu.has(m.tag)?N(ze,{children:[" ",v," "]}):N(ze,{children:v})},onText(m,h,v){let _=Math.max(h,a),S=Math.min(v,s);if(!(_>=S))return ns(m,{ranges:c},_,S)}});return N(ze,{children:[a>0&&"...",l]})}function is(e,t={highlight:!0}){ts.value=t;let r=new Worker(e);r.onmessage=n=>{let o=n.data;switch(o.type){case 1:ua.value=!0;break;case 3:typeof o.data.pagination.prev<"u"?st.value=He(k({},st.value),{pagination:o.data.pagination,items:[...st.value.items,...o.data.items]}):(st.value=o.data,mo(0));break}},Kt(()=>{pr.value&&r.postMessage({type:0,data:pr.value})}),Kt(()=>{ua.value&&r.postMessage({type:2,data:Oe.value})})}var pa={container:"p",hidden:"v"};function Bu(e){return N("div",{class:qt(pa.container,{[pa.hidden]:e.hidden}),onClick:()=>Vt()})}var fa={container:"r",disabled:"c"};function ma(e){return N("button",{class:qt(fa.container,{[fa.disabled]:!e.onClick}),onClick:e.onClick,children:e.children})}var da=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),Gu=e=>e.replace(/^([A-Z])|[\s-_]+(\w)/g,(t,r,n)=>n?n.toUpperCase():r.toLowerCase()),ha=e=>{let t=Gu(e);return t.charAt(0).toUpperCase()+t.slice(1)},Yu=(...e)=>e.filter((t,r,n)=>!!t&&t.trim()!==""&&n.indexOf(t)===r).join(" ").trim(),Ju={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},Xu=c=>{var l=c,{color:e="currentColor",size:t=24,strokeWidth:r=2,absoluteStrokeWidth:n,children:o,iconNode:i,class:a=""}=l,s=_r(l,["color","size","strokeWidth","absoluteStrokeWidth","children","iconNode","class"]);return kt("svg",k(He(k({},Ju),{width:String(t),height:t,stroke:e,"stroke-width":n?Number(r)*24/Number(t):r,class:["lucide",a].join(" ")}),s),[...i.map(([u,f])=>kt(u,f)),...$r(o)])},as=(e,t)=>{let r=a=>{var s=a,{class:n="",children:o}=s,i=_r(s,["class","children"]);return kt(Xu,He(k({},i),{iconNode:t,class:Yu(`lucide-${da(ha(e))}`,`lucide-${da(e)}`,n)}),o)};return r.displayName=ha(e),r},Zu=as("list-filter",[["path",{d:"M2 5h20",key:"1fs1ex"}],["path",{d:"M6 12h12",key:"8npq4p"}],["path",{d:"M9 19h6",key:"456am0"}]]),Qu=as("search",[["path",{d:"m21 21-4.34-4.34",key:"14j7rj"}],["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}]]),Aw=Pl(Il(),1);function ep({threshold:e=0,root:t=null,rootMargin:r="0%",freezeOnceVisible:n=!1,initialIsIntersecting:o=!1,onChange:i}={}){var a;let[s,c]=Tn(null),[l,u]=Tn(()=>({isIntersecting:o,entry:void 0})),f=zt();f.current=i;let p=((a=l.entry)==null?void 0:a.isIntersecting)&&n;ht(()=>{if(!s||!("IntersectionObserver"in window)||p)return;let v,_=new IntersectionObserver(S=>{let x=Array.isArray(_.thresholds)?_.thresholds:[_.thresholds];S.forEach(g=>{let ee=g.isIntersecting&&x.some(ue=>g.intersectionRatio>=ue);u({isIntersecting:ee,entry:g}),f.current&&f.current(ee,g),ee&&n&&v&&(v(),v=void 0)})},{threshold:e,root:t,rootMargin:r});return _.observe(s),()=>{_.disconnect()}},[s,JSON.stringify(e),t,r,p,n]);let m=zt(null);ht(()=>{var v;!s&&(v=l.entry)!=null&&v.target&&!n&&!p&&m.current!==l.entry.target&&(m.current=l.entry.target,u({isIntersecting:o,entry:void 0}))},[s,l.entry,n,p,o]);let h=[c,!!l.isIntersecting,l.entry];return h.ref=h[0],h.isIntersecting=h[1],h.entry=h[2],h}var mt={container:"n",hidden:"l",content:"y",pop:"d",badge:"w",sidebar:"e",controls:"k",results:"z",loadmore:"j"};function tp(e){let{isIntersecting:t,ref:r}=ep({threshold:0});ht(()=>{t&&Tu()},[t]);let n=zt(null);ht(()=>{n.current&&typeof Oe.value.page>"u"&&n.current.scrollTo({top:0,behavior:"smooth"})},[Oe.value]);let o=Qa();return N("div",{class:qt(mt.container,{[mt.hidden]:e.hidden}),children:[N("div",{class:mt.content,children:[N("div",{class:mt.controls,children:[N(ma,{onClick:Vt,children:N(Qu,{})}),N(np,{focus:!e.hidden}),N(ma,{onClick:Xa,children:[N(Zu,{}),o.length>0&&N("span",{class:mt.badge,children:o.length})]})]}),N("div",{class:mt.results,ref:n,children:[N(op,{keyboard:!e.hidden}),N("div",{class:mt.loadmore,ref:r})]})]}),N("div",{class:qt(mt.sidebar,{[mt.hidden]:Au()}),children:N(rp,{})})]})}var Ot={container:"X",list:"F",heading:"I",title:"R",item:"o",active:"g",value:"q",count:"A"};function rp(e){let t=ku();return t.sort((r,n)=>n.node.count-r.node.count),N("div",{class:Ot.container,children:[N("h3",{class:Ot.heading,children:"Filters"}),N("h4",{class:Ot.title,children:"Tags"}),N("ol",{class:Ot.list,children:t.map(r=>N("li",{class:qt(Ot.item,{[Ot.active]:Su(r.node.value)}),onClick:()=>Ou(r.node.value),children:[N("span",{class:Ot.value,children:r.node.value}),N("span",{class:Ot.count,children:r.node.count})]},r.node.value))})]})}var va={container:"f"};function np(e){let t=zt(null);return ht(()=>{var r,n;e.focus?(r=t.current)==null||r.focus():(n=t.current)==null||n.blur()},[e.focus]),N("div",{class:va.container,children:N("input",{ref:t,type:"text",class:va.content,value:_n(kn()),onInput:r=>Za(Ru(r.currentTarget.value)),autocapitalize:"off",autocomplete:"off",autocorrect:"off",placeholder:"Search",spellcheck:!1,role:"combobox"})})}var at={container:"b",heading:"B",item:"i",active:"h",wrapper:"C",meta:"D",actions:"s",title:"x",path:"t",excerpt:"u",more:"E"};function ss(){let[e,t]=Tn(!1);return ht(()=>{let r=()=>t(!0),n=()=>t(!1);return document.addEventListener("compositionstart",r),document.addEventListener("compositionend",n),()=>{document.removeEventListener("compositionstart",r),document.removeEventListener("compositionend",n)}},[]),e}function op(e){var s;let t=Eu(),r=Lu(),n=yn(),o=zt([]),i=ss();ht(()=>{let c=o.current[n];c&&c.scrollIntoView({block:"center",behavior:"smooth"})},[n]),Ua(e.keyboard,c=>{if(i)return;let l=yn();c.key==="ArrowDown"?(c.preventDefault(),mo(Math.min(l+1,r.length-1))):c.key==="ArrowUp"&&(c.preventDefault(),mo(Math.max(l-1,0)))},[e.keyboard,i]);let a=(s=Mu())!=null?s:0;return N(ze,{children:[r.length>0&&N("h3",{class:at.heading,children:[N("span",{class:at.bubble,children:new Intl.NumberFormat("en-US").format(a)})," ","results"]}),N("ol",{class:at.container,children:r.map((c,l)=>{var _,S,x,g;let u=c.matches.find(({field:ee})=>ee==="text"),f=os(t[c.id].title,c.matches.find(({field:ee})=>ee==="title")),p=qu((_=t[c.id].path)!=null?_:[],c.matches.find(({field:ee})=>ee==="path")),m=Ku(t[c.id].text,u),h=Math.max(0,((g=(x=(S=u==null?void 0:u.value.highlight)==null?void 0:S.ranges)==null?void 0:x.filter(ee=>ee.start{o.current[l]=ee},href:v,onClick:()=>Vt(),class:qt(at.item,{[at.active]:l===yn()}),children:N("div",{class:at.wrapper,children:[N("div",{class:at.meta,children:N("menu",{class:at.path,children:p.map((ee,ue)=>N("li",{children:ee},ue))})}),N("h2",{class:at.title,children:f}),m&&N("div",{class:at.excerpt,children:m})]})})},c.id)})})]})}var ip={container:"a"};function ap(e){let t=ss();return Ua(!0,r=>{var n,o,i,a,s;if(!t)if((r.metaKey||r.ctrlKey)&&r.key==="k")r.preventDefault(),Vt();else if((r.metaKey||r.ctrlKey)&&r.key==="j")document.body.classList.toggle("dark");else if(r.key==="Enter"&&!lr()){r.preventDefault();let c=yn(),l=(o=(n=st.value)==null?void 0:n.items[c])==null?void 0:o.id;if((a=(i=pr.value)==null?void 0:i.items[l])!=null&&a.location){Vt();let u=(s=pr.value)==null?void 0:s.items[l].location;if(es()){let f=encodeURIComponent(kn()),[p,m]=u.split("#",2);u=`${p}?h=${f.replace(/%20/g,"+")}`,typeof m<"u"&&(u+=`#${m}`)}window.location.href=u}}else r.key==="Escape"&&!lr()&&(r.preventDefault(),Vt())},[t]),N("div",{class:ip.container,children:[N(Bu,{hidden:lr()}),N(tp,{hidden:lr()})]})}function cs(e,t){wu(e),Vl(N(ap,{}),t)}function wo(){Vt()}function sp(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function cp(){return R(b(window,"compositionstart").pipe(d(()=>!0)),b(window,"compositionend").pipe(d(()=>!1))).pipe(J(!1))}function ls(){let e=b(window,"keydown").pipe(d(t=>({mode:lr()?"global":"search",type:t.key,meta:t.ctrlKey||t.metaKey,claim(){t.preventDefault(),t.stopPropagation()}})),O(({mode:t,type:r})=>{if(t==="global"){let n=Et();if(typeof n!="undefined")return!sp(n,r)}return!0}),xe());return cp().pipe(y(t=>t?w:e))}function Ue(){return new URL(location.href)}function vt(e,t=!1){if(X("navigation.instant")&&!t){let r=A("a",{href:e.href});document.body.appendChild(r),r.click(),r.remove()}else location.href=e.href}function us(){return new I}function ps(){return location.hash.slice(1)}function fs(e){let t=A("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function Eo(e){return R(b(window,"hashchange"),e).pipe(d(ps),J(ps()),O(t=>t.length>0),d(decodeURIComponent),re(1))}function ms(e){return Eo(e).pipe(d(t=>we(`[id="${t}"]`)),O(t=>typeof t!="undefined"))}function jr(e){let t=matchMedia(e);return un(r=>t.addListener(()=>r(t.matches))).pipe(J(t.matches))}function ds(){let e=matchMedia("print");return R(b(window,"beforeprint").pipe(d(()=>!0)),b(window,"afterprint").pipe(d(()=>!1))).pipe(J(e.matches))}function To(e,t){return e.pipe(y(r=>r?t():w))}function So(e,t){return new F(r=>{let n=new XMLHttpRequest;return n.open("GET",`${e}`),n.responseType="blob",n.addEventListener("load",()=>{n.status>=200&&n.status<300?(r.next(n.response),r.complete()):r.error(new Error(n.statusText))}),n.addEventListener("error",()=>{r.error(new Error("Network error"))}),n.addEventListener("abort",()=>{r.complete()}),typeof(t==null?void 0:t.progress$)!="undefined"&&(n.addEventListener("progress",o=>{var i;if(o.lengthComputable)t.progress$.next(o.loaded/o.total*100);else{let a=(i=n.getResponseHeader("Content-Length"))!=null?i:0;t.progress$.next(o.loaded/+a*100)}}),t.progress$.next(5)),n.send(),()=>n.abort()})}function et(e,t){return So(e,t).pipe(y(r=>r.text()),d(r=>JSON.parse(r)),re(1))}function An(e,t){let r=new DOMParser;return So(e,t).pipe(y(n=>n.text()),d(n=>r.parseFromString(n,"text/html")),re(1))}function hs(e,t){let r=new DOMParser;return So(e,t).pipe(y(n=>n.text()),d(n=>r.parseFromString(n,"text/xml")),re(1))}var Oo={drawer:Y("[data-md-toggle=drawer]"),search:Y("[data-md-toggle=search]")};function Lo(e,t){Oo[e].checked!==t&&Oo[e].click()}function Cn(e){let t=Oo[e];return b(t,"change").pipe(d(()=>t.checked),J(t.checked))}function vs(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function bs(){return R(b(window,"scroll",{passive:!0}),b(window,"resize",{passive:!0})).pipe(d(vs),J(vs()))}function gs(){return{width:innerWidth,height:innerHeight}}function ys(){return b(window,"resize",{passive:!0}).pipe(d(gs),J(gs()))}function _s(){return oe([bs(),ys()]).pipe(d(([e,t])=>({offset:e,size:t})),re(1))}function Hn(e,{viewport$:t,header$:r}){let n=t.pipe(ve("size")),o=oe([n,r]).pipe(d(()=>Tt(e)));return oe([r,t,o]).pipe(d(([{height:i},{offset:a,size:s},{x:c,y:l}])=>({offset:{x:a.x-c,y:a.y-l+i},size:s})))}var lp=Y("#__config"),hr=JSON.parse(lp.textContent);hr.base=`${new URL(hr.base,Ue())}`;function Ne(){return hr}function X(e){return hr.features.includes(e)}function Gt(e,t){return typeof t!="undefined"?hr.translations[e].replace("#",t.toString()):hr.translations[e]}function bt(e,t=document){return Y(`[data-md-component=${e}]`,t)}function Te(e,t=document){return $(`[data-md-component=${e}]`,t)}function up(e){let t=Y(".md-typeset > :first-child",e);return b(t,"click",{once:!0}).pipe(d(()=>Y(".md-typeset",e)),d(r=>({hash:__md_hash(r.innerHTML)})))}function xs(e){if(!X("announce.dismiss")||!e.childElementCount)return w;if(!e.hidden){let t=Y(".md-typeset",e);__md_hash(t.innerHTML)===__md_get("__announce")&&(e.hidden=!0)}return j(()=>{let t=new I;return t.subscribe(({hash:r})=>{e.hidden=!0,__md_set("__announce",r)}),up(e).pipe(P(r=>t.next(r)),q(()=>t.complete()),d(r=>k({ref:e},r)))})}function pp(e,{target$:t}){return t.pipe(d(r=>({hidden:r!==e})))}function ws(e,t){let r=new I;return r.subscribe(({hidden:n})=>{e.hidden=n}),pp(e,t).pipe(P(n=>r.next(n)),q(()=>r.complete()),d(n=>k({ref:e},n)))}function Mo(e,t){return t==="inline"?A("div",{class:"md-tooltip md-tooltip--inline",id:e,role:"tooltip"},A("div",{class:"md-tooltip__inner md-typeset"})):A("div",{class:"md-tooltip",id:e,role:"tooltip"},A("div",{class:"md-tooltip__inner md-typeset"}))}function $n(...e){return A("div",{class:"md-tooltip2",role:"dialog"},A("div",{class:"md-tooltip2__inner md-typeset"},e))}function Es(...e){return A("div",{class:"md-tooltip2",role:"tooltip"},A("div",{class:"md-tooltip2__inner md-typeset"},e))}function Ts(e,t){if(t=t?`${t}_annotation_${e}`:void 0,t){let r=t?`#${t}`:void 0;return A("aside",{class:"md-annotation",tabIndex:0},Mo(t),A("a",{href:r,class:"md-annotation__index",tabIndex:-1},A("span",{"data-md-annotation-id":e})))}else return A("aside",{class:"md-annotation",tabIndex:0},Mo(t),A("span",{class:"md-annotation__index",tabIndex:-1},A("span",{"data-md-annotation-id":e})))}function Ss(e){return A("button",{class:"md-code__button",title:Gt("clipboard.copy"),"data-clipboard-target":`#${e} > code`,"data-md-type":"copy"})}function Os(){return A("button",{class:"md-code__button",title:"Toggle line selection","data-md-type":"select"})}function Ls(){return A("nav",{class:"md-code__nav"})}var dp=xr(ko());function ks(e){return A("ul",{class:"md-source__facts"},Object.entries(e).map(([t,r])=>A("li",{class:`md-source__fact md-source__fact--${t}`},typeof r=="number"?Hi(r):r)))}function Ao(e){let t=`tabbed-control tabbed-control--${e}`;return A("div",{class:t,hidden:!0},A("button",{class:"tabbed-button",tabIndex:-1,"aria-hidden":"true"}))}function As(e){return A("div",{class:"md-typeset__scrollwrap"},A("div",{class:"md-typeset__table"},e))}function hp(e){var n;let t=Ne(),r=new URL(`../${e.version}/`,t.base);return A("li",{class:"md-version__item"},A("a",{href:`${r}`,class:"md-version__link"},e.title,((n=t.version)==null?void 0:n.alias)&&e.aliases.length>0&&A("span",{class:"md-version__alias"},e.aliases[0])))}function Cs(e,t){var n;let r=Ne();return e=e.filter(o=>{var i;return!((i=o.properties)!=null&&i.hidden)}),A("div",{class:"md-version"},A("button",{class:"md-version__current","aria-label":Gt("select.version")},t.title,((n=r.version)==null?void 0:n.alias)&&t.aliases.length>0&&A("span",{class:"md-version__alias"},t.aliases[0])),A("ul",{class:"md-version__list"},e.map(hp)))}var vp=0;function bp(e,t=250){let r=oe([cr(e),Dt(e,t)]).pipe(d(([o,i])=>o||i),ce()),n=j(()=>Ri(e)).pipe(se(Wt),kr(1),Ze(r),d(()=>ji(e)));return r.pipe(Lr(o=>o),y(()=>oe([r,n])),d(([o,i])=>({active:o,offset:i})),xe())}function Fr(e,t,r=250){let{content$:n,viewport$:o}=t,i=`__tooltip2_${vp++}`;return j(()=>{let a=new I,s=new Kn(!1);a.pipe(be(),_e(!1)).subscribe(s);let c=s.pipe(Or(u=>Ve(+!u*250,Yn)),ce(),y(u=>u?n:w),P(u=>u.id=i),xe());oe([a.pipe(d(({active:u})=>u)),c.pipe(y(u=>Dt(u,250)),J(!1))]).pipe(d(u=>u.some(f=>f))).subscribe(s);let l=s.pipe(O(u=>u),fe(c,o),d(([u,f,{size:p}])=>{let m=e.getBoundingClientRect(),h=m.width/2;if(f.role==="tooltip")return{x:h,y:8+m.height};if(m.y>=p.height/2){let{height:v}=Ae(f);return{x:h,y:-16-v}}else return{x:h,y:16+m.height}}));return oe([c,a,l]).subscribe(([u,{offset:f},p])=>{u.style.setProperty("--md-tooltip-host-x",`${f.x}px`),u.style.setProperty("--md-tooltip-host-y",`${f.y}px`),u.style.setProperty("--md-tooltip-x",`${p.x}px`),u.style.setProperty("--md-tooltip-y",`${p.y}px`),u.classList.toggle("md-tooltip2--top",p.y<0),u.classList.toggle("md-tooltip2--bottom",p.y>=0)}),s.pipe(O(u=>u),fe(c,(u,f)=>f),O(u=>u.role==="tooltip")).subscribe(u=>{let f=Ae(Y(":scope > *",u));u.style.setProperty("--md-tooltip-width",`${f.width}px`),u.style.setProperty("--md-tooltip-tail","0px")}),s.pipe(ce(),Ie(je),fe(c)).subscribe(([u,f])=>{f.classList.toggle("md-tooltip2--active",u)}),oe([s.pipe(O(u=>u)),c]).subscribe(([u,f])=>{f.role==="dialog"?(e.setAttribute("aria-controls",i),e.setAttribute("aria-haspopup","dialog")):e.setAttribute("aria-describedby",i)}),s.pipe(O(u=>!u)).subscribe(()=>{e.removeAttribute("aria-controls"),e.removeAttribute("aria-describedby"),e.removeAttribute("aria-haspopup")}),bp(e,r).pipe(P(u=>a.next(u)),q(()=>a.complete()),d(u=>k({ref:e},u)))})}function Ye(e,{viewport$:t},r=document.body){return Fr(e,{content$:new F(n=>{let o=e.title,i=Es(o);return n.next(i),e.removeAttribute("title"),r.append(i),()=>{i.remove(),e.setAttribute("title",o)}}),viewport$:t},0)}function gp(e,t){let r=j(()=>oe([Fi(e),Wt(t)])).pipe(d(([{x:n,y:o},i])=>{let{width:a,height:s}=Ae(e);return{x:n-i.x+a/2,y:o-i.y+s/2}}));return cr(e).pipe(y(n=>r.pipe(d(o=>({active:n,offset:o})),Me(+!n||1/0))))}function Hs(e,t,{target$:r}){let[n,o]=Array.from(e.children);return j(()=>{let i=new I,a=i.pipe(be(),_e(!0));return i.subscribe({next({offset:s}){e.style.setProperty("--md-tooltip-x",`${s.x}px`),e.style.setProperty("--md-tooltip-y",`${s.y}px`)},complete(){e.style.removeProperty("--md-tooltip-x"),e.style.removeProperty("--md-tooltip-y")}}),St(e).pipe(Q(a)).subscribe(s=>{e.toggleAttribute("data-md-visible",s)}),R(i.pipe(O(({active:s})=>s)),i.pipe(Ge(250),O(({active:s})=>!s))).subscribe({next({active:s}){s?e.prepend(n):n.remove()},complete(){e.prepend(n)}}),i.pipe(Xe(16,je)).subscribe(({active:s})=>{n.classList.toggle("md-tooltip--active",s)}),i.pipe(kr(125,je),O(()=>!!e.offsetParent),d(()=>e.offsetParent.getBoundingClientRect()),d(({x:s})=>s)).subscribe({next(s){s?e.style.setProperty("--md-tooltip-0",`${-s}px`):e.style.removeProperty("--md-tooltip-0")},complete(){e.style.removeProperty("--md-tooltip-0")}}),b(o,"click").pipe(Q(a),O(s=>!(s.metaKey||s.ctrlKey))).subscribe(s=>{s.stopPropagation(),s.preventDefault()}),b(o,"mousedown").pipe(Q(a),fe(i)).subscribe(([s,{active:c}])=>{var l;if(s.button!==0||s.metaKey||s.ctrlKey)s.preventDefault();else if(c){s.preventDefault();let u=e.parentElement.closest(".md-annotation");u instanceof HTMLElement?u.focus():(l=Et())==null||l.blur()}}),r.pipe(Q(a),O(s=>s===n),Ft(125)).subscribe(()=>e.focus()),gp(e,t).pipe(P(s=>i.next(s)),q(()=>i.complete()),d(s=>k({ref:e},s)))})}function yp(e){let t=Ne();if(e.tagName!=="CODE")return[e];let r=[".c",".c1",".cm"];if(t.annotate){let n=e.closest("[class|=language]");if(n)for(let o of Array.from(n.classList)){if(!o.startsWith("language-"))continue;let[,i]=o.split("-");i in t.annotate&&r.push(...t.annotate[i])}}return $(r.join(", "),e)}function _p(e){let t=[];for(let r of yp(e)){let n=[],o=document.createNodeIterator(r,NodeFilter.SHOW_TEXT);for(let i=o.nextNode();i;i=o.nextNode())n.push(i);for(let i of n){let a;for(;a=/(\(\d+\))(!)?/.exec(i.textContent);){let[,s,c]=a;if(typeof c=="undefined"){let l=i.splitText(a.index);i=l.splitText(s.length),t.push(l)}else{i.textContent=s,t.push(i);break}}}}return t}function $s(e,t){t.append(...Array.from(e.childNodes))}function Pn(e,t,{target$:r,print$:n}){let o=t.closest("[id]"),i=o==null?void 0:o.id,a=new Map;for(let s of _p(t)){let[,c]=s.textContent.match(/\((\d+)\)/);we(`:scope > li:nth-child(${c})`,e)&&(a.set(c,Ts(c,i)),s.replaceWith(a.get(c)))}return a.size===0?w:j(()=>{let s=new I,c=s.pipe(be(),_e(!0)),l=[];for(let[u,f]of a)l.push([Y(".md-typeset",f),Y(`:scope > li:nth-child(${u})`,e)]);return n.pipe(Q(c)).subscribe(u=>{e.hidden=!u,e.classList.toggle("md-annotation-list",u);for(let[f,p]of l)u?$s(f,p):$s(p,f)}),R(...[...a].map(([,u])=>Hs(u,t,{target$:r}))).pipe(q(()=>s.complete()),xe())})}function Ps(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return Ps(t)}}function Is(e,t){return j(()=>{let r=Ps(e);return typeof r!="undefined"?Pn(r,e,t):w})}var js=xr(Ho());var xp=0,Rs=R(b(window,"keydown").pipe(d(()=>!0)),R(b(window,"keyup"),b(window,"contextmenu")).pipe(d(()=>!1))).pipe(J(!1),re(1));function Fs(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return Fs(t)}}function wp(e){return Re(e).pipe(d(({width:t})=>({scrollable:Ar(e).width>t})),ve("scrollable"))}function Us(e,t){let{matches:r}=matchMedia("(hover)"),n=j(()=>{let o=new I,i=o.pipe(to(1));o.subscribe(({scrollable:m})=>{m&&r?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")});let a=[],s=e.closest("pre"),c=s.closest("[id]"),l=c?c.id:xp++;s.id=`__code_${l}`;let u=[],f=e.closest(".highlight");if(f instanceof HTMLElement){let m=Fs(f);if(typeof m!="undefined"&&(f.classList.contains("annotate")||X("content.code.annotate"))){let h=Pn(m,e,t);u.push(Re(f).pipe(Q(i),d(({width:v,height:_})=>v&&_),ce(),y(v=>v?h:w)))}}let p=$(":scope > span[id]",e);if(p.length&&(e.classList.add("md-code__content"),e.closest(".select")||X("content.code.select")&&!e.closest(".no-select"))){let m=+p[0].id.split("-").pop(),h=Os();a.push(h),X("content.tooltips")&&u.push(Ye(h,{viewport$}));let v=b(h,"click").pipe(Mr(L=>!L,!1),P(()=>h.blur()),xe());v.subscribe(L=>{h.classList.toggle("md-code__button--active",L)});let _=he(p).pipe(se(L=>Dt(L).pipe(d(M=>[L,M]))));v.pipe(y(L=>L?_:w)).subscribe(([L,M])=>{let W=we(".hll.select",L);if(W&&!M)W.replaceWith(...Array.from(W.childNodes));else if(!W&&M){let ie=document.createElement("span");ie.className="hll select",ie.append(...Array.from(L.childNodes).slice(1)),L.append(ie)}});let S=he(p).pipe(se(L=>b(L,"mousedown").pipe(P(M=>M.preventDefault()),d(()=>L)))),x=v.pipe(y(L=>L?S:w),fe(Rs),d(([L,M])=>{var ie;let W=p.indexOf(L)+m;if(M===!1)return[W,W];{let ne=$(".hll",e).map(de=>p.indexOf(de.parentElement)+m);return(ie=window.getSelection())==null||ie.removeAllRanges(),[Math.min(W,...ne),Math.max(W,...ne)]}})),g=Eo(w).pipe(O(L=>L.startsWith(`__codelineno-${l}-`)));g.subscribe(L=>{let[,,M]=L.split("-"),W=M.split(":").map(ne=>+ne-m+1);W.length===1&&W.push(W[0]);for(let ne of $(".hll:not(.select)",e))ne.replaceWith(...Array.from(ne.childNodes));let ie=p.slice(W[0]-1,W[1]);for(let ne of ie){let de=document.createElement("span");de.className="hll",de.append(...Array.from(ne.childNodes).slice(1)),ne.append(de)}}),g.pipe(Me(1),Ie(ye)).subscribe(L=>{if(L.includes(":")){let M=document.getElementById(L.split(":")[0]);M&&setTimeout(()=>{let W=M,ie=-64;for(;W!==document.body;)ie+=W.offsetTop,W=W.offsetParent;window.scrollTo({top:ie})},1)}});let ue=he($('a[href^="#__codelineno"]',f)).pipe(se(L=>b(L,"click").pipe(P(M=>M.preventDefault()),d(()=>L)))).pipe(Q(i),fe(Rs),d(([L,M])=>{let ie=+Y(`[id="${L.hash.slice(1)}"]`).parentElement.id.split("-").pop();if(M===!1)return[ie,ie];{let ne=$(".hll",e).map(de=>+de.parentElement.id.split("-").pop());return[Math.min(ie,...ne),Math.max(ie,...ne)]}}));R(x,ue).subscribe(L=>{let M=`#__codelineno-${l}-`;L[0]===L[1]?M+=L[0]:M+=`${L[0]}:${L[1]}`,history.replaceState({},"",M),window.dispatchEvent(new HashChangeEvent("hashchange",{newURL:window.location.origin+window.location.pathname+M,oldURL:window.location.href}))})}if(js.default.isSupported()&&(e.closest(".copy")||X("content.code.copy")&&!e.closest(".no-copy"))){let m=Ss(s.id);a.push(m),X("content.tooltips")&&u.push(Ye(m,{viewport$}))}if(a.length){let m=Ls();m.append(...a),s.insertBefore(m,e)}return wp(e).pipe(P(m=>o.next(m)),q(()=>o.complete()),d(m=>k({ref:e},m)),Ut(R(...u).pipe(Q(i))))});return X("content.lazy")?St(e).pipe(O(o=>o),Me(1),y(()=>n)):n}function Ep(e,{target$:t,print$:r}){let n=!0;return R(t.pipe(d(o=>o.closest("details:not([open])")),O(o=>e===o),d(()=>({action:"open",reveal:!0}))),r.pipe(O(o=>o||!n),P(()=>n=e.open),d(o=>({action:o?"open":"close"}))))}function Ns(e,t){return j(()=>{let r=new I;return r.subscribe(({action:n,reveal:o})=>{e.toggleAttribute("open",n==="open"),o&&e.scrollIntoView()}),Ep(e,t).pipe(P(n=>r.next(n)),q(()=>r.complete()),d(n=>k({ref:e},n)))})}var Ds,Ws,$o={},Vs=!1;function zs(e,t){return t()?it(e).pipe(me(()=>D(void 0)),d(()=>{})):D(void 0)}function Tp(e){if(!e)return[];if(e.startsWith("[")&&e.endsWith("]"))try{let t=JSON.parse(e);if(Array.isArray(t))return t.filter(r=>typeof r=="string")}catch(t){}return e.split(/[,\s]+/).map(t=>t.trim()).filter(t=>t.length>0)}function Sp(e){var i,a,s;let t=e.querySelector("[id$='--editor']"),r=e.querySelector("[id$='--run']"),n=e.querySelector("[id$='--clear']"),o=e.querySelector("[id$='--output']");if(!t||!r||!n||!o)throw new Error("Invalid Pyodide structure");return{root:e,editor:t,output:o,run:r,clear:n,source:(a=(i=t.textContent)==null?void 0:i.trimEnd())!=null?a:"",session:(s=e.dataset.session)!=null?s:"default",install:Tp(e.dataset.install),minLines:typeof e.dataset.minlines=="undefined"?0:parseInt(e.dataset.minlines),maxLines:typeof e.dataset.maxlines=="undefined"?1/0:parseInt(e.dataset.maxlines)}}function Op(e,t){return e in $o||($o[e]=t.globals.get("dict")()),$o[e]}function Nr(e,t){e.textContent=t}function Po(e){e.textContent=""}function Lp(){return new Promise(e=>requestAnimationFrame(()=>e()))}function Mp(e,t,r,n){return ut(this,null,function*(){let o=[];e.setStdout({batched(i){o.push(i),Nr(r,`${o.join(` +`)} +`)}});try{let i=yield e.runPythonAsync(t.getValue(),{globals:Op(n,e)});typeof i!="undefined"&&i!==null&&(o.push(String(i)),Nr(r,`${o.join(` +`)} +`))}catch(i){o.push(String(i)),Nr(r,`${o.join(` +`)} +`)}})}function kp(){return Ws||(Ws=zs("https://unpkg.com/pyodide@314.0.2/pyodide.js",()=>typeof loadPyodide=="undefined"||loadPyodide instanceof Element).pipe(d(()=>ut(null,null,function*(){try{let e=yield loadPyodide({indexURL:"https://cdn.jsdelivr.net/pyodide/v314.0.2/full/"});return yield e.loadPackage("micropip"),e}catch(e){return null}})),re(1))),Ws}function Ap(){Vs||(ace.define("ace/theme/zensical",["require","exports","module","ace/lib/dom"],(e,t)=>{t.isDark=!1,t.cssClass="ace-zensical",t.cssText=""}),Vs=!0)}function qs(e){return Ds||(Ds=zs("https://unpkg.com/ace-builds@1.44.0/src-noconflict/ace.js",()=>typeof ace=="undefined"||ace instanceof Element).pipe(re(1))),new F(t=>{let r=!0,n,o,i=Sp(e);i.root.setAttribute("data-md-exec-state","ready");let a=()=>Po(i.output),s=()=>ut(null,null,function*(){return o||(o=ut(null,null,function*(){if(i.root.setAttribute("data-md-exec-state","loading"),Nr(i.output,"Initializing..."),yield Lp(),!r)return null;let u=yield ln(kp()).then(f=>f);if(!r||!u)return null;if(i.install.length>0)try{let f=u.pyimport("micropip");for(let p of i.install)yield f.install(p)}catch(f){return Po(i.output),Nr(i.output,`Could not install one or more packages: ${i.install.join(", ")} +${String(f)}`),i.root.setAttribute("data-md-exec-state","error"),null}return r?(Po(i.output),i.root.setAttribute("data-md-exec-state","ready"),u):null})),o}),c=()=>{ut(null,null,function*(){let u=yield s();!r||!u||!n||Mp(u,n,i.output,i.session)})},l=u=>{u.ctrlKey&&u.key.toLowerCase()==="enter"&&(u.preventDefault(),i.run.click())};return i.run.addEventListener("click",c),i.clear.addEventListener("click",a),i.root.addEventListener("keydown",l),ut(null,null,function*(){yield ln(Ds),r&&(Ap(),i.editor.textContent="",n=ace.edit(i.editor),n.setTheme("ace/theme/zensical"),n.session.setMode("ace/mode/python"),n.setOption("fontFamily","var(--md-code-font)"),n.setOption("minLines",i.minLines),n.setOption("maxLines",i.maxLines),n.session.setValue(i.source),n.gotoLine(1,0,!1),n.clearSelection(),n.resize(),n.renderer.updateFull())}),t.next({ref:i.root}),()=>{r=!1,i.run.removeEventListener("click",c),i.clear.removeEventListener("click",a),i.root.removeEventListener("keydown",l),n==null||n.destroy(),i.root.removeAttribute("data-md-exec-state")}})}var Ks;function Cp(){return typeof GLightbox=="undefined"||GLightbox instanceof Element?it("https://unpkg.com/glightbox@3/dist/js/glightbox.min.js").pipe(me(()=>w),d(()=>{})):D(void 0)}function Hp(){return $i("https://unpkg.com/glightbox@3/dist/css/glightbox.min.css").pipe(me(()=>w),d(()=>{}))}function Bs(e){return Ks||(Ks=Cp().pipe(d(()=>new GLightbox(k({touchNavigation:!0,loop:!1,zoomable:!0,draggable:!0,openEffect:"zoom",closeEffect:"zoom",slideEffect:"slide",onOpen:()=>{document.activeElement instanceof HTMLElement&&document.activeElement.blur()}},typeof GLightboxOptions!="undefined"?GLightboxOptions:{}))),re(1))),Hp().pipe(y(()=>Ks),y(t=>(t.reload(),e.map(r=>({ref:r})))))}var Gs=0,Ys=new Map;function $p(e){let t=document.createElement("h3");t.innerHTML=e.innerHTML;let r=[t],n=e.nextElementSibling;for(;n&&!(n instanceof HTMLHeadingElement);)r.push(n.cloneNode(!0)),n=n.nextElementSibling;return r}function Pp(e,t){for(let r of $("[href], [src]",e))for(let n of["href","src"]){let o=r.getAttribute(n);if(o&&!/^(?:[a-z]+:)?\/\//i.test(o)){r[n]=new URL(r.getAttribute(n),t).toString();break}}for(let r of $("[name^=__], [for]",e))for(let n of["id","for","name"]){let o=r.getAttribute(n);o&&r.setAttribute(n,`${o}$preview_${Gs}`)}return Gs++,D(e)}function Ip(e){let t=Ys.get(e.toString());return t?D(t):An(e).pipe(y(r=>Pp(r,e)),d(r=>(Ys.set(e.toString(),r),r)))}function Js(e,t){let{sitemap$:r}=t;if(!(e instanceof HTMLAnchorElement))return w;if(!(X("navigation.instant.preview")||e.hasAttribute("data-preview")))return w;e.removeAttribute("title");let n=oe([cr(e),Dt(e).pipe(ke(1))]).pipe(d(([i,a])=>i||a),ce(),O(i=>i));return Rt([r,n]).pipe(y(([i])=>{let a=new URL(e.href);return a.search=a.hash="",i.has(`${a}`)?D(a):w}),y(i=>Ip(i)),y(i=>{let a=e.hash?`article [id="${decodeURIComponent(e.hash.slice(1))}"]`:"article h1",s=we(a,i);return typeof s=="undefined"?w:D($p(s))})).pipe(y(i=>{let a=new F(s=>{let c=$n(...i);return s.next(c),document.body.append(c),()=>c.remove()});return Fr(e,k({content$:a},t))}))}var Xs=".node circle,.node ellipse,.node path,.node polygon,.node rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.marker{fill:var(--md-mermaid-edge-color);stroke:var(--md-mermaid-edge-color)}.edgeLabel .label rect{fill:#0000}.flowchartTitleText{fill:var(--md-mermaid-label-fg-color)}.label{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.label foreignObject{line-height:normal;overflow:visible}.label div .edgeLabel{color:var(--md-mermaid-label-fg-color)}.edgeLabel,.edgeLabel p,.label div .edgeLabel{background-color:var(--md-mermaid-label-bg-color)}.edgeLabel,.edgeLabel p{fill:var(--md-mermaid-label-bg-color);color:var(--md-mermaid-edge-color)}.edgePath .path,.flowchart-link{stroke:var(--md-mermaid-edge-color)}.edgePath .arrowheadPath{fill:var(--md-mermaid-edge-color);stroke:none}.cluster rect{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}.cluster span{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}g #flowchart-circleEnd,g #flowchart-circleStart,g #flowchart-crossEnd,g #flowchart-crossStart,g #flowchart-pointEnd,g #flowchart-pointStart{stroke:none}.classDiagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.classGroup line,g.classGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.classGroup text{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.classLabel .box{fill:var(--md-mermaid-label-bg-color);background-color:var(--md-mermaid-label-bg-color);opacity:1}.classLabel .label{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node .divider{stroke:var(--md-mermaid-node-fg-color)}.relation{stroke:var(--md-mermaid-edge-color)}.cardinality{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.cardinality text{fill:inherit!important}defs marker.marker.composition.class path,defs marker.marker.dependency.class path,defs marker.marker.extension.class path{fill:var(--md-mermaid-edge-color)!important;stroke:var(--md-mermaid-edge-color)!important}defs marker.marker.aggregation.class path{fill:var(--md-mermaid-label-bg-color)!important;stroke:var(--md-mermaid-edge-color)!important}.statediagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.stateGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.stateGroup .state-title{fill:var(--md-mermaid-label-fg-color)!important;font-family:var(--md-mermaid-font-family)}g.stateGroup .composit{fill:var(--md-mermaid-label-bg-color)}.nodeLabel,.nodeLabel p{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}a .nodeLabel{text-decoration:underline}.node circle.state-end,.node circle.state-start,.start-state{fill:var(--md-mermaid-edge-color);stroke:none}.end-state-inner,.end-state-outer{fill:var(--md-mermaid-edge-color)}.end-state-inner,.node circle.state-end{stroke:var(--md-mermaid-label-bg-color)}.transition{stroke:var(--md-mermaid-edge-color)}[id^=state-fork] rect,[id^=state-join] rect{fill:var(--md-mermaid-edge-color)!important;stroke:none!important}.statediagram-cluster.statediagram-cluster .inner{fill:var(--md-default-bg-color)}.statediagram-cluster rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.statediagram-state rect.divider{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}defs [id$=-barbEnd]{fill:var(--md-mermaid-edge-color);stroke:var(--md-mermaid-edge-color)}[id^=entity] path,[id^=entity] rect{fill:var(--md-default-bg-color)}.relationshipLine{stroke:var(--md-mermaid-edge-color)}defs .marker.oneOrMore.er *,defs .marker.onlyOne.er *,defs .marker.zeroOrMore.er *,defs .marker.zeroOrOne.er *{stroke:var(--md-mermaid-edge-color)!important}text:not([class]):last-child{fill:var(--md-mermaid-label-fg-color)}.actor{fill:var(--md-mermaid-sequence-actor-bg-color);stroke:var(--md-mermaid-sequence-actor-border-color)}text.actor>tspan{fill:var(--md-mermaid-sequence-actor-fg-color);font-family:var(--md-mermaid-font-family)}.actor-line{stroke:var(--md-mermaid-sequence-actor-line-color)}.actor-man circle,.actor-man line{fill:var(--md-mermaid-sequence-actorman-bg-color);stroke:var(--md-mermaid-sequence-actorman-line-color)}.messageLine0,.messageLine1{stroke:var(--md-mermaid-sequence-message-line-color)}.note{fill:var(--md-mermaid-sequence-note-bg-color);stroke:var(--md-mermaid-sequence-note-border-color)}.loopText,.loopText>tspan,.messageText,.noteText>tspan{stroke:none;font-family:var(--md-mermaid-font-family)!important}.messageText{fill:var(--md-mermaid-sequence-message-fg-color)}.loopText,.loopText>tspan,.sectionTitle{fill:var(--md-mermaid-sequence-loop-fg-color)}.noteText>tspan{fill:var(--md-mermaid-sequence-note-fg-color)}[id$=-arrowhead] path{fill:var(--md-mermaid-sequence-message-line-color);stroke:none}.loopLine{fill:var(--md-mermaid-sequence-loop-bg-color);stroke:var(--md-mermaid-sequence-loop-border-color)}.labelBox{fill:var(--md-mermaid-sequence-label-bg-color);stroke:none}.labelText,.labelText>span{fill:var(--md-mermaid-sequence-label-fg-color);font-family:var(--md-mermaid-font-family)}.sequenceNumber{fill:var(--md-mermaid-sequence-number-fg-color)}rect.rect{fill:var(--md-mermaid-sequence-box-bg-color);stroke:none}rect.rect+text.text{fill:var(--md-mermaid-sequence-box-fg-color)}[id$=-sequencenumber],defs #sequencenumber{fill:var(--md-mermaid-sequence-number-bg-color)!important}[class^=activation]{fill:var(--md-mermaid-label-bg-color);stroke:var(--md-mermaid-label-fg-color);stroke-width:1.5px}";var Io,jp=0;function Fp(){return typeof mermaid=="undefined"||mermaid instanceof Element?it("https://unpkg.com/mermaid@11/dist/mermaid.min.js"):D(void 0)}function Zs(e){return e.classList.remove("mermaid"),Io||(Io=Fp().pipe(P(()=>mermaid.initialize({startOnLoad:!1,themeCSS:Xs,sequence:{actorFontSize:"16px",messageFontSize:"16px",noteFontSize:"16px"}})),d(()=>{}),re(1))),Io.subscribe(()=>ut(null,null,function*(){e.classList.add("mermaid");let t=`__mermaid_${jp++}`,r=A("div",{class:"mermaid"}),n=e.textContent,{svg:o,fn:i}=yield mermaid.render(t,n),a=r.attachShadow({mode:"closed"});a.innerHTML=o,e.replaceWith(r),i==null||i(a)})),Io.pipe(d(()=>({ref:e})))}var Qs=A("table");function ec(e){return e.replaceWith(Qs),Qs.replaceWith(As(e)),D({ref:e})}function Up(e){let t=e.find(r=>r.checked)||e[0];return R(...e.map(r=>b(r,"change").pipe(d(()=>Y(`label[for="${r.id}"]`))))).pipe(J(Y(`label[for="${t.id}"]`)),d(r=>({active:r})))}function tc(e,{viewport$:t,target$:r}){let n=Y(".tabbed-labels",e),o=$(":scope > input",e),i=Ao("prev");e.append(i);let a=Ao("next");return e.append(a),j(()=>{let s=new I,c=s.pipe(be(),_e(!0));oe([s,Re(e),St(e)]).pipe(Q(c),Xe(1,je)).subscribe({next([{active:l},u]){let f=Tt(l),{width:p}=Ae(l);e.style.setProperty("--md-indicator-x",`${f.x}px`),e.style.setProperty("--md-indicator-width",`${p}px`);let m=dn(n);(f.xm.x+u.width)&&n.scrollTo({left:Math.max(0,f.x-16),behavior:"smooth"})},complete(){e.style.removeProperty("--md-indicator-x"),e.style.removeProperty("--md-indicator-width")}}),oe([Wt(n),Re(n)]).pipe(Q(c)).subscribe(([l,u])=>{let f=Ar(n);i.hidden=l.x<16,a.hidden=l.x>f.width-u.width-16}),R(b(i,"click").pipe(d(()=>-1)),b(a,"click").pipe(d(()=>1))).pipe(Q(c)).subscribe(l=>{let{width:u}=Ae(n);n.scrollBy({left:u*l,behavior:"smooth"})}),r.pipe(Q(c),O(l=>o.includes(l))).subscribe(l=>l.click()),n.classList.add("tabbed-labels--linked");for(let l of o){let u=Y(`label[for="${l.id}"]`);u.replaceChildren(A("a",{href:`#${u.htmlFor}`,tabIndex:-1},...Array.from(u.childNodes))),b(u.firstElementChild,"click").pipe(Q(c),O(f=>!(f.metaKey||f.ctrlKey)),P(f=>{f.preventDefault(),f.stopPropagation()})).subscribe(()=>{history.replaceState({},"",`#${u.htmlFor}`),u.click()})}return X("content.tabs.link")&&s.pipe(ke(1),fe(t)).subscribe(([{active:l},{offset:u}])=>{let f=l.innerText.trim();if(l.hasAttribute("data-md-switching"))l.removeAttribute("data-md-switching");else{let p=e.offsetTop-u.y;for(let h of $("[data-tabs]"))for(let v of $(":scope > input",h)){let _=Y(`label[for="${v.id}"]`);if(_!==l&&_.innerText.trim()===f){_.setAttribute("data-md-switching",""),v.click();break}}window.scrollTo({top:e.offsetTop-p});let m=__md_get("__tabs")||[];__md_set("__tabs",[...new Set([f,...m])])}}),s.pipe(Q(c)).subscribe(()=>{for(let l of $("audio, video",e))l.offsetWidth&&l.autoplay?l.play().catch(()=>{}):l.pause()}),Up(o).pipe(P(l=>s.next(l)),q(()=>s.complete()),d(l=>k({ref:e},l)))}).pipe(It(ye))}function rc(e,t){let{viewport$:r,target$:n,print$:o}=t;return R(...$(".annotate:not(.highlight)",e).map(i=>Is(i,{target$:n,print$:o})),...$(".pyodide",e).map(i=>qs(i)),...$("pre:not(.mermaid) > code",e).filter(i=>!i.closest(".pyodide")).map(i=>Us(i,{target$:n,print$:o})),...$("a",e).map(i=>Js(i,t)),...$("pre.mermaid",e).map(i=>Zs(i)),...[$(".glightbox",e)].filter(i=>i.length>0).map(i=>Bs(i)),...$("table:not([class])",e).map(i=>ec(i)),...$("details",e).map(i=>Ns(i,{target$:n,print$:o})),...$("[data-tabs]",e).map(i=>tc(i,{viewport$:r,target$:n})),...$("[title]:not([data-preview])",e).filter(()=>X("content.tooltips")).map(i=>Ye(i,{viewport$:r})),...$(".footnote-ref",e).filter(()=>X("content.footnote.tooltips")).map(i=>Fr(i,{content$:new F(a=>{let s=new URL(i.href).hash.slice(1),c=Array.from(document.getElementById(s).cloneNode(!0).children),l=$n(...c);return a.next(l),document.body.append(l),()=>l.remove()}),viewport$:r})))}function Np(e,{alert$:t}){return t.pipe(y(r=>R(D(!0),D(!1).pipe(Ft(2e3))).pipe(d(n=>({message:r,active:n})))))}function nc(e,t){let r=Y(".md-typeset",e);return j(()=>{let n=new I;return n.subscribe(({message:o,active:i})=>{e.classList.toggle("md-dialog--active",i),r.textContent=o}),Np(e,t).pipe(P(o=>n.next(o)),q(()=>n.complete()),d(o=>k({ref:e},o)))})}function Dp({viewport$:e}){if(!X("header.autohide"))return D(!1);let t=e.pipe(d(({offset:{y:o}})=>o),jt(2,1),d(([o,i])=>[oMath.abs(i-o.y)>100),d(([,[o]])=>o),ce()),n=Cn("search");return oe([e,n]).pipe(d(([{offset:o},i])=>o.y>400&&!i),ce(),y(o=>o?r:D(!1)),J(!1))}function oc(e,t){return j(()=>oe([Re(e),Dp(t)])).pipe(d(([{height:r},n])=>({height:r,hidden:n})),ce((r,n)=>r.height===n.height&&r.hidden===n.hidden),re(1))}function ic(e,{viewport$:t,header$:r,main$:n}){return j(()=>{let o=new I,i=o.pipe(be(),_e(!0));o.pipe(ve("active"),Ze(r)).subscribe(([{active:s},{hidden:c}])=>{e.classList.toggle("md-header--shadow",s&&!c),e.hidden=c});let a=he($("[title]",e)).pipe(O(()=>X("content.tooltips")),se(s=>Ye(s,{viewport$:t})));return n.subscribe(o),r.pipe(Q(i),d(s=>k({ref:e},s)),Ut(a.pipe(Q(i))))})}function Wp(e,{viewport$:t,header$:r}){return Hn(e,{viewport$:t,header$:r}).pipe(d(({offset:{y:n}})=>{let{height:o}=Ae(e);return{active:o>0&&n>=o}}),ve("active"))}function ac(e,t){return j(()=>{let r=new I;r.subscribe({next({active:o}){e.classList.toggle("md-header__title--active",o)},complete(){e.classList.remove("md-header__title--active")}});let n=we(".md-content h1");return typeof n=="undefined"?w:Wp(n,t).pipe(P(o=>r.next(o)),q(()=>r.complete()),d(o=>k({ref:e},o)))})}function sc(e,{viewport$:t,header$:r}){let n=r.pipe(d(({height:i})=>i),ce()),o=n.pipe(y(()=>Re(e).pipe(d(({height:i})=>({top:e.offsetTop,bottom:e.offsetTop+i})),ve("bottom"))));return oe([n,o,t]).pipe(d(([i,{top:a,bottom:s},{offset:{y:c},size:{height:l}}])=>(l=Math.max(0,l-Math.max(0,a-c,i)-Math.max(0,l+c-s)),{offset:a-i,height:l,active:a-i<=c})),ce((i,a)=>i.offset===a.offset&&i.height===a.height&&i.active===a.active))}function Vp(e){let t=__md_get("__palette")||{index:e.findIndex(n=>matchMedia(n.getAttribute("data-md-color-media")).matches)},r=Math.max(0,Math.min(t.index,e.length-1));return D(...e).pipe(se(n=>b(n,"change").pipe(d(()=>n))),J(e[r]),d(n=>({index:e.indexOf(n),color:{media:n.getAttribute("data-md-color-media"),scheme:n.getAttribute("data-md-color-scheme"),primary:n.getAttribute("data-md-color-primary"),accent:n.getAttribute("data-md-color-accent")}})),re(1))}function cc(e){let t=$("input",e),r=A("meta",{name:"theme-color"});document.head.appendChild(r);let n=A("meta",{name:"color-scheme"});document.head.appendChild(n);let o=jr("(prefers-color-scheme: light)");return j(()=>{let i=new I;return i.subscribe(a=>{if(document.body.setAttribute("data-md-color-switching",""),a.color.media==="(prefers-color-scheme)"){let s=matchMedia("(prefers-color-scheme: light)"),c=document.querySelector(s.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");a.color.scheme=c.getAttribute("data-md-color-scheme"),a.color.primary=c.getAttribute("data-md-color-primary"),a.color.accent=c.getAttribute("data-md-color-accent")}for(let[s,c]of Object.entries(a.color))document.body.setAttribute(`data-md-color-${s}`,c);for(let s=0;sa.key==="Enter"),fe(i,(a,s)=>s)).subscribe(({index:a})=>{a=(a+1)%t.length,t[a].click(),t[a].focus()}),i.pipe(d(()=>{let a=bt("header"),s=window.getComputedStyle(a);return n.content=s.colorScheme,s.backgroundColor.match(/\d+/g).map(c=>(+c).toString(16).padStart(2,"0")).join("")})).subscribe(a=>r.content=`#${a}`),i.pipe(Ie(ye)).subscribe(()=>{document.body.removeAttribute("data-md-color-switching")}),Vp(t).pipe(Q(o.pipe(ke(1))),Nt(),P(a=>i.next(a)),q(()=>i.complete()),d(a=>k({ref:e},a)))})}function lc(e,{progress$:t}){return j(()=>{let r=new I;return r.subscribe(({value:n})=>{e.style.setProperty("--md-progress-value",`${n}`)}),t.pipe(P(n=>r.next({value:n})),q(()=>r.complete()),d(n=>({ref:e,value:n})))})}var uc='.m u{text-decoration:underline!important;text-decoration-style:wavy!important;text-decoration-thickness:1px!important}.p{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background-color:rgba(var(--color-backdrop)/var(--alpha-lighter));cursor:pointer;height:100%;pointer-events:auto;position:absolute;transition:opacity .25s;width:100%}.p.v{opacity:0;pointer-events:none;transition:opacity .35s}.r{align-items:center;background-color:initial;border:none;border-radius:var(--space-2);cursor:pointer;display:flex;flex-shrink:0;font-family:var(--font-family);height:36px;justify-content:center;outline:none;padding:0;position:relative;transition:background-color .25s,color .25s;width:36px;z-index:1}.r svg{stroke:rgb(var(--color-foreground));height:18px;opacity:.5;width:18px}.r:before{background-color:rgb(var(--color-background-subtle));border-radius:var(--border-radius-2);content:"";inset:0;opacity:0;position:absolute;transform:scale(.75);transition:transform 125ms,opacity 125ms;z-index:0}.r:hover:before{opacity:1;transform:scale(1)}.r.c{cursor:auto}.r.c:before{display:none}.n{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background-color:rgba(var(--color-background)/var(--alpha-light));border:1px solid rgb(var(--color-foreground)/var(--alpha-lightest));border-radius:var(--space-3);box-shadow:0 0 60px #0000000d;display:flex;height:480px;overflow:hidden;pointer-events:auto;position:absolute;transition:transform .25s cubic-bezier(.16,1,.3,1),opacity .25s;width:640px}.n.l{opacity:0;pointer-events:none;transform:scale(1.1);transition:transform .25s .15s,opacity .15s}@media (max-width:680px){.n{border-radius:0;height:100%;width:100%}}.y{display:flex;flex:1 1 auto;flex-direction:column;min-height:0;min-width:0}@keyframes d{0%{transform:scale(0)}50%{transform:scale(1.2)}to{transform:scale(1)}}.w{animation:d .25s ease-in-out;background:var(--color-highlight);border-radius:100%;color:#fff;font-size:8px;font-weight:700;height:12px;padding-top:1px;position:absolute;right:4px;top:4px;width:12px}.e{background-color:rgb(var(--color-background-subtle)/var(--alpha-lighter));border-left:1px solid rgb(var(--color-foreground)/var(--alpha-lightest));flex-shrink:0;overflow-y:scroll;position:relative;transition:width .35s cubic-bezier(.16,1,.3,1),opacity .25s;width:200px}.e>*{transform:translate(0);transition:transform .25s cubic-bezier(.16,1,.3,1)}.e.l{opacity:0;width:0}.e.l>*{transform:translate(-48px)}@media (max-width:680px){.e{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background-color:rgba(var(--color-background-subtle)/var(--alpha-light));box-shadow:0 0 60px #00000026;height:100%;position:absolute;right:0;top:0}}.k{border-bottom:1px solid rgb(var(--color-foreground)/var(--alpha-lightest));display:flex;gap:var(--space-1);padding:var(--space-2)}.z{-webkit-overflow-scrolling:touch;flex:1 1 auto;min-height:0;overflow:auto;overscroll-behavior:contain}.j{padding:8px 10px}.X{color:rgb(var(--color-foreground)/var(--alpha-light));padding:var(--space-2);position:absolute;width:100%}.F,.X{display:flex;flex-direction:column}.F{gap:2px;list-style:none;padding:0}.F,.I{margin:0}.I{font-size:16px;font-weight:400}.I,.R{padding:8px}.R{font-size:14px;margin:4px 0 0;opacity:.5}.R,.o{font-size:12px}.o{cursor:pointer;display:flex;padding:4px 8px;position:relative}.o:before{background-color:var(--color-highlight-transparent);border-radius:var(--space-1);content:"";inset:0;opacity:0;position:absolute;transform:scale(.75);transition:transform 125ms,opacity 125ms;z-index:0}.o.g:before,.o:hover:before{opacity:1;transform:scale(1)}.o.g,.o:hover{color:var(--color-highlight)}.q{flex-grow:1}.A,.q{position:relative}.A{font-weight:700}.f{flex-grow:1}.f input{background:#0000;border:none;color:rgb(var(--color-foreground));font-family:var(--font-family);font-size:16px;height:100%;letter-spacing:-.25px;outline:none;width:100%}.b{color:rgb(var(--color-foreground)/var(--alpha-light));display:flex;flex-direction:column;gap:2px;line-height:1.3;list-style:none;margin:var(--space-2);margin-top:0;padding:0}.B,.b li{margin:0}.B{color:rgb(var(--color-foreground)/var(--alpha-lighter));font-size:12px;margin-top:var(--space-2);padding:0 18px}.i{border-radius:var(--space-2);color:inherit;cursor:pointer;display:flex;flex-direction:row;flex-grow:1;padding:8px 10px;position:relative;text-decoration:none}.i:before{background-color:rgb(var(--color-background-subtle));border-radius:var(--border-radius-2);content:"";display:block;inset:0;opacity:0;position:absolute;transform:scale(.9);transition:transform 125ms,opacity 125ms;z-index:0}@media (pointer:fine){.i.h:before,.i:hover:before{opacity:1;transform:scale(1)}}.i mark{background:#0000;color:var(--color-highlight)}.i u{text-decoration:underline}.C{flex-direction:column;flex-grow:1;gap:5.5px}.C,.D{display:flex;min-width:0}.D{align-items:flex-start;gap:var(--space-2);justify-content:space-between}.s{align-items:flex-end;align-self:flex-end;display:flex;flex-direction:column;gap:6px;justify-content:flex-end;margin-right:-8px;margin-top:auto;opacity:0;position:relative;transform:translate(-2px);transition:transform 125ms,opacity 125ms;z-index:0}@media (pointer:fine){.h>.s,:hover>.s{opacity:1;transform:none}}.x{font-size:14px;margin:0;position:relative}.x code{background:rgb(var(--color-background-subtle));border-radius:var(--space-1);font-size:13px;padding:2px 4px}.t{color:rgb(var(--color-foreground)/.45);display:inline-flex;flex:1 1 auto;flex-wrap:wrap;font-size:12px;gap:var(--space-1);list-style:none;margin:0;min-width:0;padding:0;position:relative}.t li{white-space:nowrap}.t li:after{content:"/";display:inline;margin-left:var(--space-1)}.t li:last-child:after{content:"";display:none}.u{-webkit-box-orient:vertical;-webkit-line-clamp:2;color:rgb(var(--color-foreground));display:-webkit-box;font-size:12px;line-height:1.5;overflow:hidden;position:relative}.u code{background:rgb(var(--color-background-subtle));border-radius:var(--space-1);padding:2px 4px}.E,.u code{font-size:11px}.E{color:rgb(var(--color-foreground)/.45);line-height:1;white-space:nowrap;z-index:1}.a{--space-1:4px;--space-2:calc(var(--space-1)*2);--space-3:calc(var(--space-2)*2);--space-4:calc(var(--space-3)*2);--space-5:calc(var(--space-4)*2);--alpha-light:.7;--alpha-lighter:.54;--alpha-lightest:.07;--color-highlight:var(--md-accent-fg-color,#526cfe);--color-highlight-transparent:var( --md-accent-fg-color--transparent,#526cfe1a );--border-radius-1:var(--space-1);--border-radius-2:var(--space-2);--border-radius-3:calc(var(--space-1) + var(--space-2));--font-family:var( --md-text-font-family,Inter,Roboto Flex,system-ui,sans-serif );--font-size:16px;--line-height:1.5;--letter-spacing:-.5px;-webkit-font-smoothing:antialiased;align-items:center;display:flex;font-family:var(--font-family);font-size:var(--font-size);height:100vh;justify-content:center;letter-spacing:var(--letter-spacing);line-height:var(--line-height);pointer-events:none;position:absolute;width:100vw}@media (pointer:coarse){.a{height:-webkit-fill-available}}.a *,.a :after,.a :before{box-sizing:border-box}';function pc(e,{index$:t}){let r=Ne(),n=document.createElement("div");document.body.appendChild(n),n.style.position="fixed",n.style.height="100%",n.style.top="0",n.style.zIndex="4";let o=n.attachShadow({mode:"open"});o.appendChild(A("style",{},uc.toString()));try{is(r.search,{highlight:r.features.includes("search.highlight")}),he(t).subscribe(i=>{for(let a of i.items)a.location=new URL(`./${a.location}`,r.base).toString();cs(i,o)}),b(e,"click").subscribe(()=>{wo()}),Cn("search").pipe(ke(1)).subscribe(()=>wo())}catch(i){e.hidden=!0;let a=Y("label[for=__search]");a.hidden=!0}return Be}var fc=xr(ko());function mc(e,{index$:t,location$:r}){return oe([t,r.pipe(J(Ue()),O(n=>!!n.searchParams.get("h")))]).pipe(d(([n,o])=>qp(n.config)(o.searchParams.get("h"))),d(n=>{var a;let o=new Map,i=document.createNodeIterator(e,NodeFilter.SHOW_TEXT);for(let s=i.nextNode();s;s=i.nextNode())if((a=s.parentElement)!=null&&a.offsetHeight){let c=s.textContent,l=n(c);l.length>c.length&&o.set(s,l)}for(let[s,c]of o){let{childNodes:l}=A("span",null,c);s.replaceWith(...Array.from(l))}return{ref:e,nodes:o}}))}function qp(e){let t=e.separator.split("|").map(o=>o.replace(/(\(\?[!=<][^)]+\))/g,"").length===0?"\uFFFD":o).join("|"),r=new RegExp(t,"img"),n=(o,i,a)=>`${i}${a}`;return o=>{o=o.replace(/\s+/g," ").replace(/&/g,"&").trim();let i=new RegExp(`(^|${e.separator}|)(${o.split(r).map(a=>a.replace(/[|\\{}()[\]^$+*?.-]/g,"\\$&")).filter(a=>a.length>=2).join("|")})`,"img");return a=>(0,fc.default)(a).replace(i,n).replace(/<\/mark>(\s+)]*>/img,"$1")}}function Kp(e,{viewport$:t,main$:r}){let n=e.closest(".md-grid"),o=n.offsetTop-n.parentElement.offsetTop;return oe([r,t]).pipe(d(([{offset:i,height:a},{offset:{y:s}}])=>(a=a+Math.min(o,Math.max(0,s-i))-o,{height:a,locked:s>=i+o})),ce((i,a)=>i.height===a.height&&i.locked===a.locked))}function Ro(e,n){var o=n,{header$:t}=o,r=_r(o,["header$"]);let i=Y(".md-sidebar__scrollwrap",e),{y:a}=Tt(i);return j(()=>{let s=new I,c=s.pipe(be(),_e(!0)),l=s.pipe(Xe(0,je));return l.pipe(fe(t)).subscribe({next([{height:u},{height:f}]){i.style.height=`${u-2*a}px`,e.style.top=`${f}px`},complete(){i.style.height="",e.style.top=""}}),l.pipe(Lr()).subscribe(()=>{for(let u of $(".md-nav__link--active[href]",e)){if(!u.clientHeight)continue;let f=u.closest(".md-sidebar__scrollwrap");if(typeof f!="undefined"){let p=u.offsetTop-f.offsetTop,{height:m}=Ae(f);f.scrollTo({top:p-m/2})}}}),he($("label[tabindex]",e)).pipe(se(u=>b(u,"click").pipe(Ie(ye),d(()=>u),Q(c)))).subscribe(u=>{let f=Y(`[id="${u.htmlFor}"]`);Y(`[aria-labelledby="${u.id}"]`).setAttribute("aria-expanded",`${f.checked}`)}),X("content.tooltips")&&he($("abbr[title]",e)).pipe(se(u=>Ye(u,{viewport$})),Q(c)).subscribe(),Kp(e,r).pipe(P(u=>s.next(u)),q(()=>s.complete()),d(u=>k({ref:e},u)))})}function dc(e,t){if(typeof t!="undefined"){let r=`https://api.github.com/repos/${e}/${t}`;return Rt(et(`${r}/releases/latest`).pipe(me(()=>w),d(n=>({version:n.tag_name})),ot({})),et(r).pipe(me(()=>w),d(n=>({stars:n.stargazers_count,forks:n.forks_count})),ot({}))).pipe(d(([n,o])=>k(k({},n),o)))}else{let r=`https://api.github.com/users/${e}`;return et(r).pipe(d(n=>({repositories:n.public_repos})),ot({}))}}function hc(e,t){let r=`https://${e}/api/v4/projects/${encodeURIComponent(t)}`;return Rt(et(`${r}/releases/permalink/latest`).pipe(me(()=>w),d(({tag_name:n})=>({version:n})),ot({})),et(r).pipe(me(()=>w),d(({star_count:n,forks_count:o})=>({stars:n,forks:o})),ot({}))).pipe(d(([n,o])=>k(k({},n),o)))}function vc(e){let t=e.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i);if(t){let[,r,n]=t;return dc(r,n)}if(t=e.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i),t){let[,r,n]=t;return hc(r,n)}return w}var Bp;function Gp(e){return Bp||(Bp=j(()=>{let t=__md_get("__source",sessionStorage);if(t)return D(t);if(Te("consent").length){let n=__md_get("__consent");if(!(n&&n.github))return w}return vc(e.href).pipe(P(n=>__md_set("__source",n,sessionStorage)))}).pipe(me(()=>w),O(t=>Object.keys(t).length>0),d(t=>({facts:t})),re(1)))}function bc(e){let t=Y(":scope > :last-child",e);return j(()=>{let r=new I;return r.subscribe(({facts:n})=>{t.appendChild(ks(n)),t.classList.add("md-source__repository--active")}),Gp(e).pipe(P(n=>r.next(n)),q(()=>r.complete()),d(n=>k({ref:e},n)))})}function Yp(e,{viewport$:t,header$:r}){return Re(document.body).pipe(y(()=>Hn(e,{header$:r,viewport$:t})),d(({offset:{y:n}})=>({hidden:n>=10})),ve("hidden"))}function gc(e,t){return j(()=>{let r=new I;return r.subscribe({next({hidden:n}){e.hidden=n},complete(){e.hidden=!1}}),(X("navigation.tabs.sticky")?D({hidden:!1}):Yp(e,t)).pipe(P(n=>r.next(n)),q(()=>r.complete()),d(n=>k({ref:e},n)))})}function Jp(e,{viewport$:t,header$:r}){let n=new Map,o=$(".md-nav__link",e);for(let s of o){let c=decodeURIComponent(s.hash.substring(1)),l=we(`[id="${c}"]`);typeof l!="undefined"&&n.set(s,l)}let i=r.pipe(ve("height"),d(({height:s})=>{let c=bt("main"),l=Y(":scope > :first-child",c);return s+.9*(l.offsetTop-c.offsetTop)}),xe());return Re(document.body).pipe(ve("height"),y(s=>j(()=>{let c=[];return D([...n].reduce((l,[u,f])=>{for(;c.length&&n.get(c[c.length-1]).tagName>=f.tagName;)c.pop();let p=f.offsetTop;for(;!p&&f.parentElement;)f=f.parentElement,p=f.offsetTop;let m=f.offsetParent;for(;m;m=m.offsetParent)p+=m.offsetTop;return l.set([...c=[...c,u]].reverse(),p)},new Map))}).pipe(d(c=>new Map([...c].sort(([,l],[,u])=>l-u))),Ze(i),y(([c,l])=>t.pipe(Mr(([u,f],{offset:{y:p},size:m})=>{let h=p+m.height>=Math.floor(s.height);for(;f.length;){let[,v]=f[0];if(v-l=p&&!h)f=[u.pop(),...f];else break}return[u,f]},[[],[...c]]),ce((u,f)=>u[0]===f[0]&&u[1]===f[1])))))).pipe(d(([s,c])=>({prev:s.map(([l])=>l),next:c.map(([l])=>l)})),J({prev:[],next:[]}),jt(2,1),d(([s,c])=>s.prev.length{let i=new I,a=i.pipe(be(),_e(!0));i.subscribe(({prev:c,next:l})=>{for(let[u]of l)u.classList.remove("md-nav__link--passed"),u.classList.remove("md-nav__link--active");for(let[u,[f]]of c.entries())f.classList.add("md-nav__link--passed"),f.classList.toggle("md-nav__link--active",u===c.length-1)});let s=we(".md-sidebar--secondary");if(typeof s!="undefined"&&b(document.body,"click").subscribe(c=>{let l=c.target;if(!s.contains(l)){let u=we(".md-nav__toggle",s);typeof u!="undefined"&&(u.checked=!1)}}),X("toc.follow")){let c=R(t.pipe(Ge(1),d(()=>{})),t.pipe(Ge(250),d(()=>"smooth")));i.pipe(O(({prev:l})=>l.length>0),Ze(n.pipe(Ie(ye))),fe(c)).subscribe(([[{prev:l}],u])=>{let[f]=l[l.length-1];if(f.offsetHeight){let p=Ii(f);if(typeof p!="undefined"){let m=f.offsetTop-p.offsetTop,{height:h}=Ae(p);p.scrollTo({top:m-h/2,behavior:u})}}})}return X("navigation.tracking")&&t.pipe(Q(a),ve("offset"),Ge(250),ke(1),Q(o.pipe(ke(1))),Nt({delay:250}),fe(i)).subscribe(([,{prev:c}])=>{let l=Ue(),u=c[c.length-1];if(u&&u.length){let[f]=u,{hash:p}=new URL(f.href);l.hash!==p&&(l.hash=p,history.replaceState({},"",`${l}`))}else l.hash="",history.replaceState({},"",`${l}`)}),Jp(e,{viewport$:t,header$:r}).pipe(P(c=>i.next(c)),q(()=>i.complete()),d(c=>k({ref:e},c)))})}function Xp(e,{viewport$:t,main$:r,target$:n}){let o=t.pipe(d(({offset:{y:a}})=>a),jt(2,1),d(([a,s])=>a>s&&s>0),ce()),i=r.pipe(d(({active:a})=>a));return oe([i,o]).pipe(d(([a,s])=>!(a&&s)),ce(),Q(n.pipe(ke(1))),_e(!0),Nt({delay:250}),d(a=>({hidden:a})))}function _c(e,{viewport$:t,header$:r,main$:n,target$:o}){let i=new I,a=i.pipe(be(),_e(!0));return i.subscribe({next({hidden:s}){e.hidden=s,s?(e.setAttribute("tabindex","-1"),e.blur()):e.removeAttribute("tabindex")},complete(){e.style.top="",e.hidden=!0,e.removeAttribute("tabindex")}}),r.pipe(Q(a),ve("height")).subscribe(({height:s})=>{e.style.top=`${s+16}px`}),b(e,"click").subscribe(s=>{s.preventDefault(),window.scrollTo({top:0})}),Xp(e,{viewport$:t,main$:n,target$:o}).pipe(P(s=>i.next(s)),q(()=>i.complete()),d(s=>k({ref:e},s)))}function xc(e,t){return e.protocol=t.protocol,e.hostname=t.hostname,t.port&&(e.port=t.port),e}function Zp(e,t){let r=new Map;for(let n of $("url",e)){let o=Y("loc",n),i=[xc(new URL(o.textContent),t)];r.set(`${i[0]}`,i);for(let a of $("[rel=alternate]",n)){let s=a.getAttribute("href");s!=null&&i.push(xc(new URL(s),t))}}return r}function vr(e){return hs(new URL("sitemap.xml",e)).pipe(d(t=>Zp(t,new URL(e))),me(()=>D(new Map)),xe())}function wc({document$:e}){let t=new Map;e.pipe(y(()=>$("link[rel=alternate]")),d(r=>new URL(r.href)),O(r=>!t.has(r.toString())),se(r=>vr(r).pipe(d(n=>[r,n]),me(()=>w)))).subscribe(([r,n])=>{t.set(r.toString().replace(/\/$/,""),n)}),b(document.body,"click").pipe(O(r=>!r.metaKey&&!r.ctrlKey),y(r=>{if(r.target instanceof Element){let n=r.target.closest("a");if(n&&!n.target){let o=[...t].find(([f])=>n.href.startsWith(`${f}/`));if(typeof o=="undefined")return w;let[i,a]=o,s=Ue();if(s.href.startsWith(i))return w;let c=Ne(),l=s.href.replace(c.base,"");l=`${i}/${l}`;let u=a.has(l.split("#")[0])?new URL(l,c.base):new URL(i);return r.preventDefault(),D(u)}}return w})).subscribe(r=>vt(r,!0))}var jo=xr(Ho());function Qp(e){e.setAttribute("data-md-copying","");let t=e.closest("[data-copy]"),r=t?t.getAttribute("data-copy"):e.innerText;return e.removeAttribute("data-md-copying"),r.trimEnd()}function Ec({alert$:e}){jo.default.isSupported()&&new F(t=>{new jo.default("[data-clipboard-target], [data-clipboard-text]",{text:r=>r.getAttribute("data-clipboard-text")||Qp(Y(r.getAttribute("data-clipboard-target")))}).on("success",r=>t.next(r))}).pipe(P(t=>{t.trigger.focus()}),d(()=>Gt("clipboard.copied"))).subscribe(e)}function Tc(e,t){if(!(e.target instanceof Element))return w;let r=e.target.closest("a");if(r===null)return w;if(r.target||e.metaKey||e.ctrlKey)return w;let n=new URL(r.href);return n.search=n.hash="",t.has(`${n}`)?(e.preventDefault(),D(r)):w}function Sc(e){let t=new Map;for(let r of $(":scope > *",e.head))t.set(r.outerHTML,r);return t}function Oc(e){for(let t of $("[href], [src]",e))for(let r of["href","src"]){let n=t.getAttribute(r);if(n&&!/^(?:[a-z]+:)?\/\//i.test(n)){t[r]=t[r];break}}return D(e)}function ef(e){var r;if(e.tagName!=="STYLE")return!1;let t=(r=e.textContent)!=null?r:"";return t.includes(".ace_editor")||t.includes(".ace-zensical")||t.includes(".ace_scroller")||t.includes(".ace_gutter")}function tf(e){for(let n of["[data-md-component=announce]","[data-md-component=container]","[data-md-component=header-topic]","[data-md-component=outdated]","[data-md-component=logo]","[data-md-component=skip]",...X("navigation.tabs.sticky")?["[data-md-component=tabs]"]:[]]){let o=we(n),i=we(n,e);typeof o!="undefined"&&typeof i!="undefined"&&o.replaceWith(i)}let t=Sc(document);for(let[n,o]of Sc(e))t.has(n)?t.delete(n):document.head.appendChild(o);for(let n of t.values()){let o=n.getAttribute("name");o!=="theme-color"&&o!=="color-scheme"&&!ef(n)&&n.remove()}let r=bt("container");return nt($("script",r)).pipe(y(n=>{let o=e.createElement("script");if(n.src){for(let i of n.getAttributeNames())o.setAttribute(i,n.getAttribute(i));return n.replaceWith(o),new F(i=>{o.onload=()=>i.complete()})}else return o.textContent=n.textContent,n.replaceWith(o),w}),be(),_e(document))}function Lc({sitemap$:e,location$:t,viewport$:r,progress$:n}){if(location.protocol==="file:")return Be;D(document).subscribe(Oc);let o=b(document.body,"click").pipe(Ze(e),y(([s,c])=>Tc(s,c)),d(({href:s})=>new URL(s)),xe()),i=b(window,"popstate").pipe(d(Ue),xe());o.pipe(fe(r)).subscribe(([s,{offset:c}])=>{history.replaceState(c,""),history.pushState(null,"",s)}),R(o,i).subscribe(t);let a=t.pipe(J(Ue()),mn(),O(([s,c])=>s.pathname!==c.pathname),d(([,s])=>s),y(s=>An(s,{progress$:n}).pipe(me(()=>(vt(s,!0),w)))),y(Oc),y(tf),xe());return R(a.pipe(fe(t,(s,c)=>c)),a.pipe(y(()=>t),ve("hash")),t.pipe(J(Ue()),mn(),O(([s,c])=>s.pathname===c.pathname&&s.hash!==c.hash),d(([,s])=>s)),t.pipe(ce((s,c)=>s.pathname===c.pathname&&s.hash===c.hash),y(()=>o),P(()=>history.back()))).subscribe(s=>{var c,l;history.state!==null||!s.hash?window.scrollTo(0,(l=(c=history.state)==null?void 0:c.y)!=null?l:0):(history.scrollRestoration="auto",fs(s.hash),history.scrollRestoration="manual")}),t.subscribe(()=>{history.scrollRestoration="manual"}),b(window,"beforeunload").subscribe(()=>{history.scrollRestoration="auto"}),r.pipe(ve("offset"),Ge(100)).subscribe(({offset:s})=>{history.replaceState(s,"")}),X("navigation.instant.prefetch")&&R(b(document.body,"mousemove"),b(document.body,"focusin")).pipe(Ze(e),y(([s,c])=>Tc(s,c)),Ge(25),eo(({href:s})=>s),fn(s=>{let c=document.createElement("link");return c.rel="prefetch",c.href=s.toString(),document.head.appendChild(c),b(c,"load").pipe(d(()=>c),Me(1))})).subscribe(s=>s.remove()),a}function Mc(e){var u;let{selectedVersionSitemap:t,selectedVersionBaseURL:r,currentLocation:n,currentBaseURL:o}=e,i=(u=Fo(o))==null?void 0:u.pathname;if(i===void 0)return;let a=rf(n.pathname,i);if(a===void 0)return;let s=of(t.keys());if(!t.has(s))return;let c=Fo(a,s);if(!c||!t.has(c.href))return;let l=Fo(a,r);if(l)return l.hash=n.hash,l.search=n.search,l}function Fo(e,t){try{return new URL(e,t)}catch(r){return}}function rf(e,t){if(e.startsWith(t))return e.slice(t.length)}function nf(e,t){let r=Math.min(e.length,t.length),n;for(n=0;nw)),n=r.pipe(d(o=>{let[,i]=t.base.match(/([^/]+)\/?$/);return o.find(({version:a,aliases:s})=>a===i||s.includes(i))||o[0]}));r.pipe(d(o=>new Map(o.map(i=>[`${new URL(`../${i.version}/`,t.base)}`,i]))),y(o=>b(document.body,"click").pipe(O(i=>!i.metaKey&&!i.ctrlKey),fe(n),y(([i,a])=>{if(i.target instanceof Element){let s=i.target.closest("a");if(s&&!s.target&&o.has(s.href)){let c=s.href;return!i.target.closest(".md-version")&&o.get(c)===a?w:(i.preventDefault(),D(new URL(c)))}}return w}),y(i=>vr(i).pipe(d(a=>{var s;return(s=Mc({selectedVersionSitemap:a,selectedVersionBaseURL:i,currentLocation:Ue(),currentBaseURL:t.base}))!=null?s:i})))))).subscribe(o=>vt(o,!0)),oe([r,n]).subscribe(([o,i])=>{Y(".md-header__topic").appendChild(Cs(o,i))}),e.pipe(y(()=>n)).subscribe(o=>{var s;let i=new URL(t.base),a=__md_get("__outdated",sessionStorage,i);if(a===null){a=!0;let c=((s=t.version)==null?void 0:s.default)||"latest";Array.isArray(c)||(c=[c]);e:for(let l of c)for(let u of o.aliases.concat(o.version))if(new RegExp(l,"i").test(u)){a=!1;break e}__md_set("__outdated",a,sessionStorage,i)}if(a)for(let c of Te("outdated"))c.hidden=!1})}function Ac({document$:e,viewport$:t}){e.pipe(y(()=>$(".md-ellipsis")),se(r=>St(r).pipe(Q(e.pipe(ke(1))),O(n=>n),d(()=>r),Me(1))),O(r=>r.offsetWidth{let n=r.innerText,o=r.closest("a")||r;return o.title=n,X("content.tooltips")?Ye(o,{viewport$:t}).pipe(Q(e.pipe(ke(1))),q(()=>o.removeAttribute("title"))):w})).subscribe(),X("content.tooltips")&&e.pipe(y(()=>$(".md-status")),se(r=>Ye(r,{viewport$:t}))).subscribe()}function Cc({document$:e,tablet$:t}){e.pipe(y(()=>$(".md-toggle--indeterminate")),P(r=>{r.indeterminate=!0,r.checked=!1}),se(r=>b(r,"change").pipe(no(()=>r.classList.contains("md-toggle--indeterminate")),d(()=>r))),fe(t)).subscribe(([r,n])=>{r.classList.remove("md-toggle--indeterminate"),n&&(r.checked=!1)})}function af(){return/(iPad|iPhone|iPod)/.test(navigator.userAgent)}function Hc({document$:e}){e.pipe(y(()=>$("[data-md-scrollfix]")),P(t=>t.removeAttribute("data-md-scrollfix")),O(af),se(t=>b(t,"touchstart").pipe(d(()=>t)))).subscribe(t=>{let r=t.scrollTop;r===0?t.scrollTop=1:r+t.offsetHeight===t.scrollHeight&&(t.scrollTop=r-1)})}Object.entries||(Object.entries=function(e){let t=[];for(let r of Object.keys(e))t.push([r,e[r]]);return t});Object.values||(Object.values=function(e){let t=[];for(let r of Object.keys(e))t.push(e[r]);return t});typeof Element!="undefined"&&(Element.prototype.scrollTo||(Element.prototype.scrollTo=function(e,t){typeof e=="object"?(this.scrollLeft=e.left,this.scrollTop=e.top):(this.scrollLeft=e,this.scrollTop=t)}),Element.prototype.replaceWith||(Element.prototype.replaceWith=function(...e){let t=this.parentNode;if(t){e.length===0&&t.removeChild(this);for(let r=e.length-1;r>=0;r--){let n=e[r];typeof n=="string"?n=document.createTextNode(n):n.parentNode&&n.parentNode.removeChild(n),r?t.insertBefore(this.previousSibling,n):t.replaceChild(n,this)}}}));function sf(){return location.protocol==="file:"?it(`${new URL("search.js",In.base)}`).pipe(d(()=>__index),me(()=>Be),re(1)):et(new URL("search.json",In.base))}document.documentElement.classList.remove("no-js");document.documentElement.classList.add("js");var gt=Ai(),Wr=us(),br=ms(Wr),Pc=ls(),qe=_s(),Uo=jr("(min-width: 60em)"),Ic=jr("(min-width: 76.25em)"),Rc=ds(),In=Ne(),jc=we(".md-search")?sf():Be,No=new I;Ec({alert$:No});wc({document$:gt});var Do=new I,Fc=vr(In.base);X("navigation.instant")&&Lc({sitemap$:Fc,location$:Wr,viewport$:qe,progress$:Do}).subscribe(gt);var $c;(($c=In.version)==null?void 0:$c.provider)==="mike"&&kc({document$:gt});R(Wr,br).pipe(Ft(125)).subscribe(()=>{Lo("drawer",!1),Lo("search",!1)});Pc.pipe(O(({mode:e,meta:t})=>e==="global"&&!t)).subscribe(e=>{switch(e.type){case",":case"p":let t=document.querySelector("link[rel=prev]");t instanceof HTMLLinkElement&&vt(t);break;case".":case"n":let r=document.querySelector("link[rel=next]");r instanceof HTMLLinkElement&&vt(r);break;case"/":let n=document.querySelector("[data-md-component=search] button");n instanceof HTMLButtonElement&&n.click();break;case"Enter":let o=Et();o instanceof HTMLLabelElement&&o.click()}});Ac({viewport$:qe,document$:gt});Cc({document$:gt,tablet$:Uo});Hc({document$:gt});var Ct=oc(bt("header"),{viewport$:qe}),Dr=gt.pipe(d(()=>bt("main")),y(e=>sc(e,{viewport$:qe,header$:Ct})),re(1)),cf=R(...Te("consent").map(e=>ws(e,{target$:br})),...Te("dialog").map(e=>nc(e,{alert$:No})),...Te("palette").map(e=>cc(e)),...Te("progress").map(e=>lc(e,{progress$:Do})),...Te("search").map(e=>pc(e,{index$:jc})),...Te("source").map(e=>bc(e))),lf=j(()=>R(...Te("announce").map(e=>xs(e)),...Te("content").map(e=>rc(e,{sitemap$:Fc,viewport$:qe,target$:br,print$:Rc})),...Te("content").map(e=>X("search.highlight")?mc(e,{index$:jc,location$:Wr}):w),...Te("header").map(e=>ic(e,{viewport$:qe,header$:Ct,main$:Dr})),...Te("header-title").map(e=>ac(e,{viewport$:qe,header$:Ct})),...Te("sidebar").map(e=>e.getAttribute("data-md-type")==="navigation"?To(Ic,()=>Ro(e,{viewport$:qe,header$:Ct,main$:Dr})):To(Uo,()=>Ro(e,{viewport$:qe,header$:Ct,main$:Dr}))),...Te("tabs").map(e=>gc(e,{viewport$:qe,header$:Ct})),...Te("toc").map(e=>yc(e,{viewport$:qe,header$:Ct,main$:Dr,target$:br})),...Te("top").map(e=>_c(e,{viewport$:qe,header$:Ct,main$:Dr,target$:br})))),Uc=gt.pipe(y(()=>lf),Ut(cf),re(1));Uc.subscribe();window.document$=gt;window.location$=Wr;window.target$=br;window.keyboard$=Pc;window.viewport$=qe;window.tablet$=Uo;window.screen$=Ic;window.print$=Rc;window.alert$=No;window.progress$=Do;window.component$=Uc;})(); diff --git a/v5.1/assets/javascripts/workers/search.b6b7e04f.min.js b/v5.1/assets/javascripts/workers/search.b6b7e04f.min.js new file mode 100644 index 0000000..c93e95d --- /dev/null +++ b/v5.1/assets/javascripts/workers/search.b6b7e04f.min.js @@ -0,0 +1 @@ +"use strict";(()=>{var vt=Object.create;var K=Object.defineProperty,wt=Object.defineProperties,bt=Object.getOwnPropertyDescriptor,Tt=Object.getOwnPropertyDescriptors,Mt=Object.getOwnPropertyNames,W=Object.getOwnPropertySymbols,kt=Object.getPrototypeOf,Y=Object.prototype.hasOwnProperty,Et=Object.prototype.propertyIsEnumerable;var B=(t,e,n)=>e in t?K(t,e,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[e]=n,R=(t,e)=>{for(var n in e||(e={}))Y.call(e,n)&&B(t,n,e[n]);if(W)for(var n of W(e))Et.call(e,n)&&B(t,n,e[n]);return t},Q=(t,e)=>wt(t,Tt(e));var Ft=(t,e)=>()=>{try{return e||t((e={exports:{}}).exports,e),e.exports}catch(n){throw e=0,n}};var Rt=(t,e,n,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let l of Mt(e))!Y.call(t,l)&&l!==n&&K(t,l,{get:()=>e[l],enumerable:!(r=bt(e,l))||r.enumerable});return t};var qt=(t,e,n)=>(n=t!=null?vt(kt(t)):{},Rt(e||!t||!t.__esModule?K(n,"default",{value:t,enumerable:!0}):n,t));var L=(t,e,n)=>B(t,typeof e!="symbol"?e+"":e,n);var E=(t,e,n)=>new Promise((r,l)=>{var o=u=>{try{s(n.next(u))}catch(i){l(i)}},a=u=>{try{s(n.throw(u))}catch(i){l(i)}},s=u=>u.done?r(u.value):Promise.resolve(u.value).then(o,a);s((n=n.apply(t,e)).next())});var xt=Ft(mt=>{"use strict";function C(t,e,n={}){return{name:t,from:e,meta:n}}function H(t,e){let n=[{value:t,depth:0}];for(let r=0,l=-1,o=0;r>=0;){let{value:a,depth:s}=n[r];if(l<=s&&a.type==="operator"&&a.data.operands.length>0)for(let u=a.data.operands.length;u>0;)n[++r]={value:a.data.operands[--u],depth:s+1};else{let u=e(a,o++,s);if(typeof u<"u")return u;--r}l=s}}var P=class extends Error{constructor(t,e){super(e),this.code=t}};function $(t,e){let n=zt(t);for(let r=0;r{let{matches:r}=n;for(let l=0;l{let l=e.get(n);return typeof l>"u"&&e.set(n,l=t(n,...r)),l}}function D(t,e){return Object.defineProperty(e,"name",{value:t}),e}function tt(t){return E(this,null,function*(){let e=[];if(typeof t.plugins<"u")for(let n=0;n32)throw new RangeError("Bit format exceeds 32 bits");return t}function nt(t,e,n){let r=N(t),l=N(e),o=typeof n<"u"?N(n):32-r-l;return St({d:r,f:l,x:o})}var T=[0];for(let t=0;t<32;t++)T.push(T[t]|1<=n&&t{e+=r*r}),Math.sqrt(e)}function Pt(t,e){t instanceof J?t.data.forEach((n,r)=>{e(n,r)}):t.forEach((n,r)=>{e({start:n,end:n+1,value:1},r)})}var O=class{constructor(t,e=jt(Math.ceil(t/32))){this.size=t,this.data=e}get(t){return this.data[t>>>5]>>>t&1}set(t){this.data[t>>>5]|=1<<(t&31)}forEach(t){let e=this.size&31;for(let n=0;n>>0;for(let n=0;n0;l++){let{value:o,depth:a}=n[--r],s=e(o,l,a);if(typeof s<"u")return s;for(let u=o.children.length;u>0;)n[r++]={value:o.children[--u],depth:a+1}}}function Vt(t,e){return E(this,null,function*(){let{fields:n,plugins:r=[]}=e,l=nt(t.length,n.length),o=[];for(let u=0;u"u")continue;let f=u<{var m;return(m=g.onFilterInput)==null?void 0:m.call(g,p,f,l)},d);let c=o[i],h=k();d=Array.isArray(d)?d:[d];for(let p=0;p{let v=c.index.get(m.node);typeof v>"u"&&c.index.set(m.node,v=k());let w=c.terms.length;for(let b=0;b{var i;return(i=u.onFilterStore)==null?void 0:i.call(u,s,e,t)}),s})}function Ut(t,e,n,r={}){let l=[];if(e<0||e>=t.count.fields)return l;let o=t.shards[e],a=new Map,{count:s=1/0,depth:u=1/0}=r;for(let i=0;iu)continue;let p=a.get(d);typeof p>"u"&&a.set(d,p={node:f,children:[]});let g=l;h>0&&(g=a.get(o.terms[c]).children),g.length=t.count.fields)return{documents:r,terms:l};let o=t.shards[n];return e.forEach(a=>{let{occurrences:s}=o.terms[a];for(let u=0;u>>t.space.x>>>t.space.f;r.set(i)}l[n].set(a)}),{documents:r,terms:l}}function Bt(t){let{documents:e,terms:n}=U(t);A(e,1);for(let r=0;rnew O(e.length))}}function Kt(t,e,n){let{compiler:r,fields:l,plugins:o=[]}=n,{input:a,scope:s,abort:u=!1}=z(o,(f,c)=>{var h;return(h=c.onFilterQuery)==null?void 0:h.call(c,f,t,n)},e),i={items:[],query:{select:U(t),values:[]}},d=new Map;if(u===!1){let f=r(n),{select:c,values:h}=f(a,t);typeof s<"u"&&V(c.documents,s);let p=new Map;i.query={select:c,values:h},c.terms.forEach((g,m)=>{g.forEach(y=>{let x=t.shards[m],{occurrences:v}=x.terms[y];for(let w=0;w>>t.space.x,F=b>>>t.space.f;if(!c.documents.get(F))continue;let S=p.get(b);typeof S>"u"&&p.set(b,S=new j(k()));let yt=M&T[t.space.x];S.add(yt,y)}})}),c.documents.forEach(g=>{let m={id:g,matches:[]};i.items.push(m),d.set(g,m)}),p.forEach((g,m)=>{let y=m>>>t.space.f,x=m&T[t.space.f];d.get(y).matches.push({id:m,field:l[x].name,value:{filter:g},score:0})})}return z(o,(f,c)=>{var h;return(h=c.onFilterResult)==null?void 0:h.call(c,f,t,n)},i)}function st(t){let{fields:e}=t;return(n,r)=>{if(It(n))return n;let l=[Bt(r)],o=[],a=0;return H(n,({type:s,data:u})=>{switch(s){case"value":let i=e.findIndex(({name:c})=>c===u.field);if(i===-1){l[a++]=U(r);break}let d=u.value;if(typeof d!="object"){let c=new j(k()),h=r.shards[i],p=h.index.get(d);if(typeof p<"u")for(let g=0;gf+1&&a--;){I(l[f].documents,l[a].documents);for(let c=0;cf+1&&a--;){V(l[f].documents,l[a].documents);for(let c=0;cf+1&&a--;)lt(l[f].documents,l[a].documents)}}}),{select:l[0],values:o}}}function Lt(t){return{name:t.name,data:t.data,onFilterOptions:t.onFilterOptions,onFilterInput:t.onFilterInput,onFilterStore:t.onFilterStore,onFilterQuery:t.onFilterQuery,onFilterResult:t.onFilterResult}}function ot(t){return typeof t=="object"&&t!==null&&"type"in t&&"data"in t}function Nt(t){return typeof t=="object"&&t!==null&&"select"in t&&"values"in t}function Gt(t){return t.normalize("NFKD").toLowerCase()}function Ht(t,e){let n=Math.min(t.length,e.length);for(let r=0;r65535)){let o=e(l=t.codePointAt(n),n);if(typeof o<"u")return o}}function ut(t,e,n=0,r=t.length){let l=k();return Jt(t,o=>{l.push(o);let a=e(String.fromCodePoint(...l),l.length);if(typeof a<"u")return a},n,r)}function Wt(t,e,n=0,r=t.length){let l=n;for(let o=0;ln&&e(n,n=l);continue;case 62:n=l+1}l>n&&e(n,l)}function it(t,e,n,r=0){return Wt(t,(l,o)=>e(t,(a,s)=>{r=n({value:t.slice(a,s),index:r,start:a,end:s})},l,o)),r}function Yt(t,e,n,r=0){for(let l=0,o=0;l(a.start+=o,a.end+=o,n(a)),r);return r}function Zt(t){let e=new RegExp(t,"gu");return(n,r,l=0,o=n.length)=>{var u;e.lastIndex=l;let a,s=0;do{a=e.exec(n);let i=(u=a==null?void 0:a.index)!=null?u:o;l"u")continue;let p=f<{var y;return(y=m.onTextInput)==null?void 0:y.call(m,g,p,a)},h),h=Array.isArray(h)?h:[h],Yt(h,n,g=>{let m=z(o,(y,x)=>{var v;return(v=x.onTextTokens)==null?void 0:v.call(x,y)},[g]);for(let y=0;y"u"?s.set(x,[p<{var c;return(c=f.onTextStore)==null?void 0:c.call(f,i,e,t)}),i})}function Xt(t,e,n){let{documents:r,terms:l}=_(t);return n<0||n>=t.count.fields?{documents:r,terms:l}:(e.forEach(o=>{let{occurrences:a}=t.terms[o];for(let s=0;s>>t.space.x;if((u&T[t.space.f])!==n)continue;let i=u>>>t.space.f;r.set(i)}l.set(o)}),{documents:r,terms:l})}function te(t,e){let{documents:n,terms:r}=_(t),l=t.space.f+t.space.x;return e.forEach(o=>{let{occurrences:a}=t.terms[o];for(let s=0;s>>l);r.set(o)}),{documents:n,terms:r}}function _(t){return{documents:new O(t.count.documents),terms:new O(t.terms.length)}}function ee(t,e,n){let{compiler:r,fields:l,plugins:o=[]}=n,{input:a,scope:s,abort:u=!1}=z(o,(f,c)=>{var h;return(h=c.onTextQuery)==null?void 0:h.call(c,f,t,n)},e),i={items:[],query:{select:_(t),values:[]}},d=new Map;if(u===!1){let f=r(n),{select:c,values:h}=f(a,t);typeof s<"u"&&V(c.documents,s);let p=new O(l.length),g=new Map;i.query={select:c,values:h},c.terms.forEach(m=>{A(p,0);for(let x=0;x>>t.space.x,M=w>>>t.space.f;if(!c.documents.get(M))continue;let b=w&T[t.space.f];if(!p.get(b))continue;let F=g.get(w);typeof F>"u"&&g.set(w,F=new j(k()));let S=v&T[t.space.x];F.add(S,m)}}),c.documents.forEach(m=>{let y={id:m,matches:[]};i.items.push(y),d.set(m,y)}),g.forEach((m,y)=>{let x=y>>>t.space.f,v=y&T[t.space.f];d.get(x).matches.push({id:y,field:l[v].name,value:{text:m},score:0})})}return z(o,(f,c)=>{var h;return(h=c.onTextResult)==null?void 0:h.call(c,f,t,n)},i)}function ne(t,e=10){return t.length>1?1+t[t.length-1]-t[0]:e}function re(t,e,n,r=10){let l=[];t.value.text.forEach((s,u)=>{for(let i=0;is.index-u.index);let o=l.slice(0,1),a=0;for(let s=0;sr||i.value===u.value)d=o.map(({index:f})=>f),o=[l[s+1]];else{for(let f=0;fi.index-u.index){let h=o.splice(f+1);d=o.map(({index:p})=>p),o=[...h,l[s+1]]}else d=o.map(({index:h})=>h),o=[l[s+1]];break}}typeof d>"u"&&o.push(l[s+1])}if(typeof d<"u"){let f=n(d,a++);if(typeof f<"u")return f}}if(o.length)return n(o.map(({index:s})=>s),a)}function le(t){let{transform:e,parser:n,fields:r}=t,l=n(t);return(o,a)=>{if(Nt(o))return o;typeof o=="string"&&(o=l(o));let s=[_(a)],u=[],i=0;return H(o,({type:d,data:f})=>{switch(d){case"value":let c=f.value;if(typeof c=="string"){let p=new j(k()),g=a.index.get(e(c));typeof g<"u"&&p.add(g,1),c=p}if(f.field==="*")s[i++]=te(a,c);else{let p=r.findIndex(({name:g})=>g===f.field);s[i++]=Xt(a,c,p)}u.push(Q(R({},f),{value:c}));break;case"operator":let h=i-f.operands.length;switch(f.operator){case"or":for(;i>h+1&&i--;)I(s[h].documents,s[i].documents),I(s[h].terms,s[i].terms);break;case"and":for(;i>h+1&&i--;)V(s[h].documents,s[i].documents),I(s[h].terms,s[i].terms);break;case"not":for(at(s[h].documents),A(s[h].terms,0);i>h+1&&i--;)lt(s[h].documents,s[i].documents)}}}),{select:s[0],values:u}}}function ft(t,e){return H(t,(n,r,l)=>{if(n.type!=="value")return;let o=e(n.data,r,l);if(typeof o<"u")return o})}function ct(t){if(t.length===0)return[];let e=[],n=[];for(let o=0;oo.index-a.index);let r=new Set([n[0].value]),l=n[0].index;for(let o=1;o{t[i].start>l||t[i].end{e.push({start:l,end:o,value:n})})}return new J(ct(e))}function ae(t,e="or",n){let{separator:r}=t;return n!=null||(n=l=>({field:"*",value:l.value})),l=>{let o=[];return it(l,r,a=>{let s=n(a);typeof s<"u"&&o.push({type:"value",data:s})}),{type:"operator",data:{operator:e,operands:o}}}}function se(t,e){return E(this,null,function*(){let n=yield tt(e),r=yield At(n,(o,a)=>{var s;return(s=a.onTextOptions)==null?void 0:s.call(a,o,t)},Q(R({},e),{plugins:n})),l=yield $t(t,r);return D("text",o=>{if(o.type!=="text")throw new P("unsupported");return{type:o.type,data:ee(l,o.data,r)}})})}function q(t){return{name:t.name,data:t.data,onTextOptions:t.onTextOptions,onTextInput:t.onTextInput,onTextTokens:t.onTextTokens,onTextStore:t.onTextStore,onTextQuery:t.onTextQuery,onTextResult:t.onTextResult}}function oe(t){let{handlers:e}=t,n,r=new Map;return Lt({name:"aggregation",onFilterStore(l,o){for(let a=0;a"u")continue;let u=!0;o.documents.forEach(i=>{u=!1}),u&&A(o.documents,1),l.aggregations.push(s(a,o))}}})}function ue(t={}){let{empty:e=!1,limit:n}=t;return(r,{fields:l})=>{let o=r.space.f+r.space.x;return D("term",({type:a,data:s},{documents:u})=>{if(a!=="term")throw new P("unsupported");let i=l.findIndex(({name:f})=>f===s.field),d=Ut(r,i,f=>{let c=0,{occurrences:h}=f;for(let p=0;p>>o)&&c++;if(!(e===!1&&c===0))return{value:f.value,count:c}},R(R({},n),s.limit));return{type:a,data:{field:s.field,value:d}}})}}function ie(t,e="prefix"){return{type:e,data:t}}function fe(t){return typeof t=="object"&&"type"in t&&typeof t.type=="string"&&"data"in t&&typeof t.data=="string"}function ce(t,e={}){var u;let{prefix:n=2,filter:r=[]}=e,l=t.terms,o=new Map,a=Ot(l.length),s=k();for(let i=0;i{var p;return o.set(c,(p=o.get(c))!=null?p:i),h===n||void 0});let f=i?l[i-1]:"";a[i]=Ht(f,d)}for(let i=0;ii-d),{terms:l,index:o,cover:a,exact:s}}function de(t,e){let n="",r=-1,l=-1;if(ut(e,s=>{let u=t.index.get(s);if(typeof u>"u")return!0;n=s,r=u}),r!==-1)for(let s=n.length;ss>r&&sa),index:e.index},{prefix:t.prefix,filter:(l=t.filter)==null?void 0:l.map(r)}))},onTextQuery(e,n,r){let{transform:l,parser:o}=r;if(typeof e.input=="string")e.input=o(r)(e.input);else if(!ot(e.input))return;ft(e.input,a=>{var u;let s=a.value;if(fe(s))s=l(s.data);else return;a.value=(u=de(this.data,s))!=null?u:s})}})}function pe(t){let e=Q(R({},t),{plugins:[]}),n,r,l;return q({name:"filter",onTextOptions(a,s){return E(this,null,function*(){e.plugins=yield tt(t),l=yield Vt(s,e)})},onTextQuery(a){typeof a.filter<"u"&&(n=a.filter,r!=null||(r=st(e)),n.input=r(n.input,l),a.scope=n.input.select.documents)},onTextResult(a){if(typeof n<"u"){let s=!0;a.query.select.documents.forEach(i=>{s=!1}),s||(n.scope=a.query.select.documents);let u=Kt(l,n,e);a.aggregations=u.aggregations,n=void 0}}})}function ht(t,e){let n=[],r=t/e>>>0,l=t%e,o=0;if(r)for(let a=0;as);r.sort((a,s)=>t.terms[a].length-t.terms[s].length||t.terms[a].localeCompare(t.terms[s]));let l=0;for(let a=0;al&&(l=t.terms[a].length);let o=[];for(let a=0;a"u"?u[d].set(f,[r[a]]):c.push(r[a])}}return{index:o,terms:t.terms,idxmp:r}}var ye=[["id","di","rr"],["dr","rd"],["dd"]];function pt(t,e,n=2){if(t.lengthn)return;let a,s,u,i=n+1;for(let d of ye[o]){for(u=a=s=0;an)break;switch(d[u-1]){case"d":a++;break;case"i":s++;break;case"r":a++,s++;break}}else a++,s++;u+=r-a+(l-s),u"u")continue;let a=me(e,o,2);for(let s=0;s"u"))for(let c of f){let h=t.terms[c].length,p=n(t.terms[c],e);typeof p<"u"&&r.add(c,(h-p)/h)}}}}if(r.data.length)return r}function we(t={}){return q({name:"fuzzy",onTextStore(e){var n;(n=this.data)!=null||(this.data=xe({terms:e.terms.map(({value:r})=>r)},t))},onTextQuery(e,n,r){let{transform:l,parser:o}=r;if(typeof e.input=="string")e.input=o(r)(e.input);else if(!ot(e.input))return;ft(e.input,a=>{var u;let s=a.value;if(typeof s=="string")s=l(s);else return;n.index.get(s)||(a.value=(u=ve(this.data,s))!=null?u:s)})}})}function be(){return{tables:new Map}}function Te(t,e={}){let{count:n}=e;return D("term",r=>{let l=dt(r);return(o,a)=>{let s=[];return o.value.text.forEach((u,i)=>{let d=a[u]>>>10,f=a[u]&T[10];for(let p=0;pu.start-i.start),{ranges:ct(s).slice(0,n)}}})}function Me(t){let e,n;return q({name:"highlight",data:be(),onTextInput(r,l){let{tables:o}=this.data;o.set(l,n=k())},onTextTokens(r){for(let l=0;l{let s=l.get(a.id);if(a.value.highlight)return;let u=o(a,s);a.value=Q(R({},a.value),{highlight:u})})}})}function ke(){return{directives:[]}}function gt(...t){return(e,n)=>{for(let r=0;r{if(r!=="match")throw new P("unsupported");let o=Fe(t),a=gt(...e.map(s=>s(n)));return $(n,({matches:s})=>{s.sort(o)}),(s,u)=>{let i=Math.min(s.matches.length,u.matches.length);for(let d=0,f=0;dr*(l.get(a.field)-l.get(s.field))}function Re(t,e={}){let n=dt(t.query),r=G(re),l=G(ne);return(o,a)=>{let s=r(o,n,f=>f),u=r(a,n,f=>f);if(s.length!==u.length)return u.length-s.length;let i=l(s),d=l(u);return i!==d?i-d:s[0]!==u[0]?s[0]-u[0]:0}}function qe(t){let e=new Map;return q({name:"order",data:ke(),onTextOptions(r,l){return E(this,null,function*(){for(let o=0;o"u")throw new P("unknown");o.push(u(r,s))}r.items.sort(gt(...o))}})}function ze(t){let e=t.handler();return q({name:"pagination",onTextQuery(n){return e.onQuery(n,t)},onTextResult(n){return e.onResult(n,t)}})}function Ae(t){let{id:e,size:n=10,from:r=0}=t;if(r-n>=0)return{id:e,size:n,from:r-n}}function Qe(t,e){let{id:n,size:r=10,from:l=0}=t;if(l+rE(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/v5.1/assets/stylesheets/classic/main.f62f0af6.min.css b/v5.1/assets/stylesheets/classic/main.f62f0af6.min.css new file mode 100644 index 0000000..fb95841 --- /dev/null +++ b/v5.1/assets/stylesheets/classic/main.f62f0af6.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[role=dialog]:after{content:"";display:block;height:.8rem;left:clamp(.8rem,var(--md-tooltip-host-x) - .8rem,100vw - var(--md-tooltip-width) - .8rem);pointer-events:auto;position:absolute;width:var(--md-tooltip-width);z-index:1}.md-tooltip2[role=dialog].md-tooltip2--top:after{top:100%}.md-tooltip2[role=dialog].md-tooltip2--bottom:after{bottom:100%}.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,\3c !--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc.-->')}.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)}.md-typeset .pyodide{background-color:var(--md-code-bg-color);border-radius:.1rem;font-family:var(--md-code-font-family);padding:.65625em 1em}.md-typeset .pyodide>pre{margin:0}.md-typeset .pyodide-editor{font-size:.85em;margin-bottom:1em;margin-top:1em;width:100%}.md-typeset .pyodide-editor-bar{border-bottom:.05rem solid var(--md-default-fg-color--lightest);color:var(--md-default-fg-color--light);font:monospace;font-size:.75em;margin-bottom:.4rem;padding-bottom:.4rem;width:100%}.md-typeset .pyodide-bar-item{display:inline-block;width:50%}.md-typeset .pyodide-clickable{cursor:pointer;text-align:right}.md-typeset .pyodide-output{background:#0000;border-radius:0;padding:0;width:100%}.md-typeset .pyodide-output code{padding:0}.md-typeset .ace-zensical{background-color:initial;color:var(--md-code-fg-color)}.md-typeset .ace-zensical .ace_gutter{background-color:initial;color:var(--md-default-fg-color--light)}.md-typeset .ace-zensical .ace_gutter-cell{padding-left:0}.md-typeset .ace-zensical .ace_cursor{color:var(--md-code-fg-color)}.md-typeset .ace-zensical .ace_selection{background:var(--md-code-hl-color--light)}.md-typeset .ace-zensical .ace_active-line{background:var(--md-default-fg-color--lightest)}.md-typeset .ace-zensical .ace_comment{color:var(--md-code-hl-comment-color)}.md-typeset .ace-zensical .ace_string{color:var(--md-code-hl-string-color)}.md-typeset .ace-zensical .ace_keyword{color:var(--md-code-hl-keyword-color)}.md-typeset .ace-zensical .ace_identifier{color:var(--md-code-fg-color)}.md-typeset .ace-zensical .ace_variable{color:var(--md-code-hl-variable-color)}.md-typeset .ace-zensical .ace_function{color:var(--md-code-hl-function-color)}.md-typeset .ace-zensical .ace_constant,.md-typeset .ace-zensical .ace_support{color:var(--md-code-hl-constant-color)}.md-typeset .ace-zensical .ace_numeric{color:var(--md-code-hl-number-color)}.md-typeset .ace-zensical .ace_operator{color:var(--md-code-hl-operator-color)}.md-typeset .ace-zensical .ace_punctuation{color:var(--md-code-hl-punctuation-color)}:root{--ansi-red:#f55;--ansi-green:#50fa7b;--ansi-blue:#265285;--ansi-yellow:#ffb86c;--ansi-magenta:#bd93f9;--ansi-cyan:#8be9fd;--ansi-black:#282a36;--ansi-white:#f8f8f2}.-Color-Bold-Green,.-Color-BrightGreen,.-Color-Faint-Green,.-Color-Green{color:var(--ansi-green)}.-Color-Bold-Red,.-Color-BrightRed,.-Color-Faint-Red,.-Color-Red{color:var(--ansi-red)}.-Color-Bold-Yellow,.-Color-BrightYellow,.-Color-Faint-Yellow,.-Color-Yellow{color:var(--ansi-yellow)}.-Color-Blue,.-Color-Bold-Blue,.-Color-BrightBlue,.-Color-Faint-Blue{color:var(--ansi-blue)}.-Color-Bold-Magenta,.-Color-BrightMagenta,.-Color-Faint-Magenta,.-Color-Magenta{color:var(--ansi-magenta)}.-Color-Bold-Cyan,.-Color-BrightCyan,.-Color-Cyan,.-Color-Faint-Cyan{color:var(--ansi-cyan)}.-Color-Bold-White,.-Color-BrightWhite,.-Color-Faint-White,.-Color-White{color:var(--ansi-white)}.-Color-Black,.-Color-Bold-Black,.-Color-BrightBlack,.-Color-Faint-Black{color:var(--ansi-black)}.-Color-Faint{opacity:.5}.-Color-Bold{font-weight:700}.-Color-BGBlack,.-Color-Black-BGBlack,.-Color-Blue-BGBlack,.-Color-Bold-BGBlack,.-Color-Bold-Black-BGBlack,.-Color-Bold-Blue-BGBlack,.-Color-Bold-Cyan-BGBlack,.-Color-Bold-Green-BGBlack,.-Color-Bold-Magenta-BGBlack,.-Color-Bold-Red-BGBlack,.-Color-Bold-White-BGBlack,.-Color-Bold-Yellow-BGBlack,.-Color-BrightBGBlack,.-Color-BrightBlack-BGBlack,.-Color-BrightBlue-BGBlack,.-Color-BrightCyan-BGBlack,.-Color-BrightGreen-BGBlack,.-Color-BrightMagenta-BGBlack,.-Color-BrightRed-BGBlack,.-Color-BrightWhite-BGBlack,.-Color-BrightYellow-BGBlack,.-Color-Cyan-BGBlack,.-Color-Green-BGBlack,.-Color-Magenta-BGBlack,.-Color-Red-BGBlack,.-Color-White-BGBlack,.-Color-Yellow-BGBlack{background-color:var(--ansi-black)}.-Color-BGRed,.-Color-Black-BGRed,.-Color-Blue-BGRed,.-Color-Bold-BGRed,.-Color-Bold-Black-BGRed,.-Color-Bold-Blue-BGRed,.-Color-Bold-Cyan-BGRed,.-Color-Bold-Green-BGRed,.-Color-Bold-Magenta-BGRed,.-Color-Bold-Red-BGRed,.-Color-Bold-White-BGRed,.-Color-Bold-Yellow-BGRed,.-Color-BrightBGRed,.-Color-BrightBlack-BGRed,.-Color-BrightBlue-BGRed,.-Color-BrightCyan-BGRed,.-Color-BrightGreen-BGRed,.-Color-BrightMagenta-BGRed,.-Color-BrightRed-BGRed,.-Color-BrightWhite-BGRed,.-Color-BrightYellow-BGRed,.-Color-Cyan-BGRed,.-Color-Green-BGRed,.-Color-Magenta-BGRed,.-Color-Red-BGRed,.-Color-White-BGRed,.-Color-Yellow-BGRed{background-color:var(--ansi-red)}.-Color-BGGreen,.-Color-Black-BGGreen,.-Color-Blue-BGGreen,.-Color-Bold-BGGreen,.-Color-Bold-Black-BGGreen,.-Color-Bold-Blue-BGGreen,.-Color-Bold-Cyan-BGGreen,.-Color-Bold-Green-BGGreen,.-Color-Bold-Magenta-BGGreen,.-Color-Bold-Red-BGGreen,.-Color-Bold-White-BGGreen,.-Color-Bold-Yellow-BGGreen,.-Color-BrightBGGreen,.-Color-BrightBlack-BGGreen,.-Color-BrightBlue-BGGreen,.-Color-BrightCyan-BGGreen,.-Color-BrightGreen-BGGreen,.-Color-BrightMagenta-BGGreen,.-Color-BrightRed-BGGreen,.-Color-BrightWhite-BGGreen,.-Color-BrightYellow-BGGreen,.-Color-Cyan-BGGreen,.-Color-Green-BGGreen,.-Color-Magenta-BGGreen,.-Color-Red-BGGreen,.-Color-White-BGGreen,.-Color-Yellow-BGGreen{background-color:var(--ansi-green)}.-Color-BGYellow,.-Color-Black-BGYellow,.-Color-Blue-BGYellow,.-Color-Bold-BGYellow,.-Color-Bold-Black-BGYellow,.-Color-Bold-Blue-BGYellow,.-Color-Bold-Cyan-BGYellow,.-Color-Bold-Green-BGYellow,.-Color-Bold-Magenta-BGYellow,.-Color-Bold-Red-BGYellow,.-Color-Bold-White-BGYellow,.-Color-Bold-Yellow-BGYellow,.-Color-BrightBGYellow,.-Color-BrightBlack-BGYellow,.-Color-BrightBlue-BGYellow,.-Color-BrightCyan-BGYellow,.-Color-BrightGreen-BGYellow,.-Color-BrightMagenta-BGYellow,.-Color-BrightRed-BGYellow,.-Color-BrightWhite-BGYellow,.-Color-BrightYellow-BGYellow,.-Color-Cyan-BGYellow,.-Color-Green-BGYellow,.-Color-Magenta-BGYellow,.-Color-Red-BGYellow,.-Color-White-BGYellow,.-Color-Yellow-BGYellow{background-color:var(--ansi-yellow)}.-Color-BGBlue,.-Color-Black-BGBlue,.-Color-Blue-BGBlue,.-Color-Bold-BGBlue,.-Color-Bold-Black-BGBlue,.-Color-Bold-Blue-BGBlue,.-Color-Bold-Cyan-BGBlue,.-Color-Bold-Green-BGBlue,.-Color-Bold-Magenta-BGBlue,.-Color-Bold-Red-BGBlue,.-Color-Bold-White-BGBlue,.-Color-Bold-Yellow-BGBlue,.-Color-BrightBGBlue,.-Color-BrightBlack-BGBlue,.-Color-BrightBlue-BGBlue,.-Color-BrightCyan-BGBlue,.-Color-BrightGreen-BGBlue,.-Color-BrightMagenta-BGBlue,.-Color-BrightRed-BGBlue,.-Color-BrightWhite-BGBlue,.-Color-BrightYellow-BGBlue,.-Color-Cyan-BGBlue,.-Color-Green-BGBlue,.-Color-Magenta-BGBlue,.-Color-Red-BGBlue,.-Color-White-BGBlue,.-Color-Yellow-BGBlue{background-color:var(--ansi-blue)}.-Color-BGMagenta,.-Color-Black-BGMagenta,.-Color-Blue-BGMagenta,.-Color-Bold-BGMagenta,.-Color-Bold-Black-BGMagenta,.-Color-Bold-Blue-BGMagenta,.-Color-Bold-Cyan-BGMagenta,.-Color-Bold-Green-BGMagenta,.-Color-Bold-Magenta-BGMagenta,.-Color-Bold-Red-BGMagenta,.-Color-Bold-White-BGMagenta,.-Color-Bold-Yellow-BGMagenta,.-Color-BrightBGMagenta,.-Color-BrightBlack-BGMagenta,.-Color-BrightBlue-BGMagenta,.-Color-BrightCyan-BGMagenta,.-Color-BrightGreen-BGMagenta,.-Color-BrightMagenta-BGMagenta,.-Color-BrightRed-BGMagenta,.-Color-BrightWhite-BGMagenta,.-Color-BrightYellow-BGMagenta,.-Color-Cyan-BGMagenta,.-Color-Green-BGMagenta,.-Color-Magenta-BGMagenta,.-Color-Red-BGMagenta,.-Color-White-BGMagenta,.-Color-Yellow-BGMagenta{background-color:var(--ansi-magenta)}.-Color-BGCyan,.-Color-Black-BGCyan,.-Color-Blue-BGCyan,.-Color-Bold-BGCyan,.-Color-Bold-Black-BGCyan,.-Color-Bold-Blue-BGCyan,.-Color-Bold-Cyan-BGCyan,.-Color-Bold-Green-BGCyan,.-Color-Bold-Magenta-BGCyan,.-Color-Bold-Red-BGCyan,.-Color-Bold-White-BGCyan,.-Color-Bold-Yellow-BGCyan,.-Color-BrightBGCyan,.-Color-BrightBlack-BGCyan,.-Color-BrightBlue-BGCyan,.-Color-BrightCyan-BGCyan,.-Color-BrightGreen-BGCyan,.-Color-BrightMagenta-BGCyan,.-Color-BrightRed-BGCyan,.-Color-BrightWhite-BGCyan,.-Color-BrightYellow-BGCyan,.-Color-Cyan-BGCyan,.-Color-Green-BGCyan,.-Color-Magenta-BGCyan,.-Color-Red-BGCyan,.-Color-White-BGCyan,.-Color-Yellow-BGCyan{background-color:var(--ansi-cyan)}.-Color-BGWhite,.-Color-Black-BGWhite,.-Color-Blue-BGWhite,.-Color-Bold-BGWhite,.-Color-Bold-Black-BGWhite,.-Color-Bold-Blue-BGWhite,.-Color-Bold-Cyan-BGWhite,.-Color-Bold-Green-BGWhite,.-Color-Bold-Magenta-BGWhite,.-Color-Bold-Red-BGWhite,.-Color-Bold-White-BGWhite,.-Color-Bold-Yellow-BGWhite,.-Color-BrightBGWhite,.-Color-BrightBlack-BGWhite,.-Color-BrightBlue-BGWhite,.-Color-BrightCyan-BGWhite,.-Color-BrightGreen-BGWhite,.-Color-BrightMagenta-BGWhite,.-Color-BrightRed-BGWhite,.-Color-BrightWhite-BGWhite,.-Color-BrightYellow-BGWhite,.-Color-Cyan-BGWhite,.-Color-Green-BGWhite,.-Color-Magenta-BGWhite,.-Color-Red-BGWhite,.-Color-White-BGWhite,.-Color-Yellow-BGWhite{background-color:var(--ansi-white)}.-Color-Black,.-Color-Black-BGBlack,.-Color-Black-BGGreen,.-Color-Blue-BGBlue,.-Color-Bold-Black,.-Color-Bold-Black-BGBlack,.-Color-Bold-Blue-BGBlue,.-Color-Bold-Red-BGRed,.-Color-BrightBlack,.-Color-BrightBlack-BGBlack,.-Color-BrightBlue-BGBlue,.-Color-BrightRed-BGRed,.-Color-Red-BGRed{text-shadow:0 0 1px var(--ansi-white)}.-Color-Bold-Cyan-BGCyan,.-Color-Bold-Green-BGGreen,.-Color-Bold-Magenta-BGMagenta,.-Color-Bold-White,.-Color-Bold-Yellow-BGYellow,.-Color-BrightCyan-BGCyan,.-Color-BrightGreen-BGGreen,.-Color-BrightMagenta-BGMagenta,.-Color-BrightWhite,.-Color-BrightYellow-BGYellow,.-Color-Cyan-BGCyan,.-Color-Cyan-BGGreen,.-Color-Green-BGCyan,.-Color-Green-BGGreen,.-Color-Magenta-BGMagenta,.-Color-White,.-Color-White-BGWhite,.-Color-Yellow-BGYellow{text-shadow:0 0 1px var(--ansi-black)}html.glightbox-open{height:100%;overflow:initial}html .gslide .gslide-description{background:var(--md-default-bg-color);-webkit-user-select:text;user-select:text}html .gslide .gslide-title{color:var(--md-default-fg-color);font-size:.8rem;margin-bottom:.4rem;margin-top:0}html .gslide .gslide-desc{color:var(--md-default-fg-color--light);font-size:.7rem}: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;font-size:.85em;padding:.7720588235em 0 .7720588235em 1.1764705882em;-webkit-user-select:none;user-select:none}.highlighttable tr:first-child>.linenos{border-top-left-radius:.1rem}.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 .highlighttable>tbody>tr:nth-child(2)>.code>div>pre>code{border-top-right-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/v5.1/assets/stylesheets/classic/palette.7dc9a0ad.min.css b/v5.1/assets/stylesheets/classic/palette.7dc9a0ad.min.css new file mode 100644 index 0000000..2d83819 --- /dev/null +++ b/v5.1/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/v5.1/assets/stylesheets/modern/main.62c9b9cc.min.css b/v5.1/assets/stylesheets/modern/main.62c9b9cc.min.css new file mode 100644 index 0000000..a2b6bfd --- /dev/null +++ b/v5.1/assets/stylesheets/modern/main.62c9b9cc.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:.8533333333em;padding:.2em .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.5;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:.8203125em 1.25em;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 .md-typeset{font-size:.7rem;line-height:1.3}.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;overflow-wrap:normal;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[role=dialog]:after{content:"";display:block;height:.8rem;left:clamp(.8rem,var(--md-tooltip-host-x) - .8rem,100vw - var(--md-tooltip-width) - .8rem);pointer-events:auto;position:absolute;width:var(--md-tooltip-width);z-index:1}.md-tooltip2[role=dialog].md-tooltip2--top:after{top:100%}.md-tooltip2[role=dialog].md-tooltip2--bottom:after{bottom:100%}.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,\3c !--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc.-->')}.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)}.md-typeset .pyodide{background-color:var(--md-code-bg-color);border-radius:.4rem;font-family:var(--md-code-font-family);padding:.7em 1.0666666667em}.md-typeset .pyodide>pre{margin:0}.md-typeset .pyodide-editor{font-size:.8533333333em;margin-bottom:1em;margin-top:1em;width:100%}.md-typeset .pyodide-editor-bar{border-bottom:.05rem solid var(--md-default-fg-color--lightest);color:var(--md-default-fg-color--light);font:monospace;font-size:.75em;margin-bottom:.4rem;padding-bottom:.4rem;width:100%}.md-typeset .pyodide-bar-item{display:inline-block;width:50%}.md-typeset .pyodide-clickable{cursor:pointer;text-align:right}.md-typeset .pyodide-output{background:#0000;border-radius:0;padding:0;width:100%}.md-typeset .pyodide-output code{padding:0}.md-typeset .ace-zensical{background-color:initial;color:var(--md-code-fg-color)}.md-typeset .ace-zensical .ace_gutter{background-color:initial;color:var(--md-default-fg-color--light)}.md-typeset .ace-zensical .ace_gutter-cell{padding-left:0}.md-typeset .ace-zensical .ace_cursor{color:var(--md-code-fg-color)}.md-typeset .ace-zensical .ace_selection{background:var(--md-code-hl-color--light)}.md-typeset .ace-zensical .ace_active-line{background:var(--md-default-fg-color--lightest)}.md-typeset .ace-zensical .ace_comment{color:var(--md-code-hl-comment-color)}.md-typeset .ace-zensical .ace_string{color:var(--md-code-hl-string-color)}.md-typeset .ace-zensical .ace_keyword{color:var(--md-code-hl-keyword-color)}.md-typeset .ace-zensical .ace_identifier{color:var(--md-code-fg-color)}.md-typeset .ace-zensical .ace_variable{color:var(--md-code-hl-variable-color)}.md-typeset .ace-zensical .ace_function{color:var(--md-code-hl-function-color)}.md-typeset .ace-zensical .ace_constant,.md-typeset .ace-zensical .ace_support{color:var(--md-code-hl-constant-color)}.md-typeset .ace-zensical .ace_numeric{color:var(--md-code-hl-number-color)}.md-typeset .ace-zensical .ace_operator{color:var(--md-code-hl-operator-color)}.md-typeset .ace-zensical .ace_punctuation{color:var(--md-code-hl-punctuation-color)}:root{--ansi-red:#f55;--ansi-green:#50fa7b;--ansi-blue:#265285;--ansi-yellow:#ffb86c;--ansi-magenta:#bd93f9;--ansi-cyan:#8be9fd;--ansi-black:#282a36;--ansi-white:#f8f8f2}.-Color-Bold-Green,.-Color-BrightGreen,.-Color-Faint-Green,.-Color-Green{color:var(--ansi-green)}.-Color-Bold-Red,.-Color-BrightRed,.-Color-Faint-Red,.-Color-Red{color:var(--ansi-red)}.-Color-Bold-Yellow,.-Color-BrightYellow,.-Color-Faint-Yellow,.-Color-Yellow{color:var(--ansi-yellow)}.-Color-Blue,.-Color-Bold-Blue,.-Color-BrightBlue,.-Color-Faint-Blue{color:var(--ansi-blue)}.-Color-Bold-Magenta,.-Color-BrightMagenta,.-Color-Faint-Magenta,.-Color-Magenta{color:var(--ansi-magenta)}.-Color-Bold-Cyan,.-Color-BrightCyan,.-Color-Cyan,.-Color-Faint-Cyan{color:var(--ansi-cyan)}.-Color-Bold-White,.-Color-BrightWhite,.-Color-Faint-White,.-Color-White{color:var(--ansi-white)}.-Color-Black,.-Color-Bold-Black,.-Color-BrightBlack,.-Color-Faint-Black{color:var(--ansi-black)}.-Color-Faint{opacity:.5}.-Color-Bold{font-weight:700}.-Color-BGBlack,.-Color-Black-BGBlack,.-Color-Blue-BGBlack,.-Color-Bold-BGBlack,.-Color-Bold-Black-BGBlack,.-Color-Bold-Blue-BGBlack,.-Color-Bold-Cyan-BGBlack,.-Color-Bold-Green-BGBlack,.-Color-Bold-Magenta-BGBlack,.-Color-Bold-Red-BGBlack,.-Color-Bold-White-BGBlack,.-Color-Bold-Yellow-BGBlack,.-Color-BrightBGBlack,.-Color-BrightBlack-BGBlack,.-Color-BrightBlue-BGBlack,.-Color-BrightCyan-BGBlack,.-Color-BrightGreen-BGBlack,.-Color-BrightMagenta-BGBlack,.-Color-BrightRed-BGBlack,.-Color-BrightWhite-BGBlack,.-Color-BrightYellow-BGBlack,.-Color-Cyan-BGBlack,.-Color-Green-BGBlack,.-Color-Magenta-BGBlack,.-Color-Red-BGBlack,.-Color-White-BGBlack,.-Color-Yellow-BGBlack{background-color:var(--ansi-black)}.-Color-BGRed,.-Color-Black-BGRed,.-Color-Blue-BGRed,.-Color-Bold-BGRed,.-Color-Bold-Black-BGRed,.-Color-Bold-Blue-BGRed,.-Color-Bold-Cyan-BGRed,.-Color-Bold-Green-BGRed,.-Color-Bold-Magenta-BGRed,.-Color-Bold-Red-BGRed,.-Color-Bold-White-BGRed,.-Color-Bold-Yellow-BGRed,.-Color-BrightBGRed,.-Color-BrightBlack-BGRed,.-Color-BrightBlue-BGRed,.-Color-BrightCyan-BGRed,.-Color-BrightGreen-BGRed,.-Color-BrightMagenta-BGRed,.-Color-BrightRed-BGRed,.-Color-BrightWhite-BGRed,.-Color-BrightYellow-BGRed,.-Color-Cyan-BGRed,.-Color-Green-BGRed,.-Color-Magenta-BGRed,.-Color-Red-BGRed,.-Color-White-BGRed,.-Color-Yellow-BGRed{background-color:var(--ansi-red)}.-Color-BGGreen,.-Color-Black-BGGreen,.-Color-Blue-BGGreen,.-Color-Bold-BGGreen,.-Color-Bold-Black-BGGreen,.-Color-Bold-Blue-BGGreen,.-Color-Bold-Cyan-BGGreen,.-Color-Bold-Green-BGGreen,.-Color-Bold-Magenta-BGGreen,.-Color-Bold-Red-BGGreen,.-Color-Bold-White-BGGreen,.-Color-Bold-Yellow-BGGreen,.-Color-BrightBGGreen,.-Color-BrightBlack-BGGreen,.-Color-BrightBlue-BGGreen,.-Color-BrightCyan-BGGreen,.-Color-BrightGreen-BGGreen,.-Color-BrightMagenta-BGGreen,.-Color-BrightRed-BGGreen,.-Color-BrightWhite-BGGreen,.-Color-BrightYellow-BGGreen,.-Color-Cyan-BGGreen,.-Color-Green-BGGreen,.-Color-Magenta-BGGreen,.-Color-Red-BGGreen,.-Color-White-BGGreen,.-Color-Yellow-BGGreen{background-color:var(--ansi-green)}.-Color-BGYellow,.-Color-Black-BGYellow,.-Color-Blue-BGYellow,.-Color-Bold-BGYellow,.-Color-Bold-Black-BGYellow,.-Color-Bold-Blue-BGYellow,.-Color-Bold-Cyan-BGYellow,.-Color-Bold-Green-BGYellow,.-Color-Bold-Magenta-BGYellow,.-Color-Bold-Red-BGYellow,.-Color-Bold-White-BGYellow,.-Color-Bold-Yellow-BGYellow,.-Color-BrightBGYellow,.-Color-BrightBlack-BGYellow,.-Color-BrightBlue-BGYellow,.-Color-BrightCyan-BGYellow,.-Color-BrightGreen-BGYellow,.-Color-BrightMagenta-BGYellow,.-Color-BrightRed-BGYellow,.-Color-BrightWhite-BGYellow,.-Color-BrightYellow-BGYellow,.-Color-Cyan-BGYellow,.-Color-Green-BGYellow,.-Color-Magenta-BGYellow,.-Color-Red-BGYellow,.-Color-White-BGYellow,.-Color-Yellow-BGYellow{background-color:var(--ansi-yellow)}.-Color-BGBlue,.-Color-Black-BGBlue,.-Color-Blue-BGBlue,.-Color-Bold-BGBlue,.-Color-Bold-Black-BGBlue,.-Color-Bold-Blue-BGBlue,.-Color-Bold-Cyan-BGBlue,.-Color-Bold-Green-BGBlue,.-Color-Bold-Magenta-BGBlue,.-Color-Bold-Red-BGBlue,.-Color-Bold-White-BGBlue,.-Color-Bold-Yellow-BGBlue,.-Color-BrightBGBlue,.-Color-BrightBlack-BGBlue,.-Color-BrightBlue-BGBlue,.-Color-BrightCyan-BGBlue,.-Color-BrightGreen-BGBlue,.-Color-BrightMagenta-BGBlue,.-Color-BrightRed-BGBlue,.-Color-BrightWhite-BGBlue,.-Color-BrightYellow-BGBlue,.-Color-Cyan-BGBlue,.-Color-Green-BGBlue,.-Color-Magenta-BGBlue,.-Color-Red-BGBlue,.-Color-White-BGBlue,.-Color-Yellow-BGBlue{background-color:var(--ansi-blue)}.-Color-BGMagenta,.-Color-Black-BGMagenta,.-Color-Blue-BGMagenta,.-Color-Bold-BGMagenta,.-Color-Bold-Black-BGMagenta,.-Color-Bold-Blue-BGMagenta,.-Color-Bold-Cyan-BGMagenta,.-Color-Bold-Green-BGMagenta,.-Color-Bold-Magenta-BGMagenta,.-Color-Bold-Red-BGMagenta,.-Color-Bold-White-BGMagenta,.-Color-Bold-Yellow-BGMagenta,.-Color-BrightBGMagenta,.-Color-BrightBlack-BGMagenta,.-Color-BrightBlue-BGMagenta,.-Color-BrightCyan-BGMagenta,.-Color-BrightGreen-BGMagenta,.-Color-BrightMagenta-BGMagenta,.-Color-BrightRed-BGMagenta,.-Color-BrightWhite-BGMagenta,.-Color-BrightYellow-BGMagenta,.-Color-Cyan-BGMagenta,.-Color-Green-BGMagenta,.-Color-Magenta-BGMagenta,.-Color-Red-BGMagenta,.-Color-White-BGMagenta,.-Color-Yellow-BGMagenta{background-color:var(--ansi-magenta)}.-Color-BGCyan,.-Color-Black-BGCyan,.-Color-Blue-BGCyan,.-Color-Bold-BGCyan,.-Color-Bold-Black-BGCyan,.-Color-Bold-Blue-BGCyan,.-Color-Bold-Cyan-BGCyan,.-Color-Bold-Green-BGCyan,.-Color-Bold-Magenta-BGCyan,.-Color-Bold-Red-BGCyan,.-Color-Bold-White-BGCyan,.-Color-Bold-Yellow-BGCyan,.-Color-BrightBGCyan,.-Color-BrightBlack-BGCyan,.-Color-BrightBlue-BGCyan,.-Color-BrightCyan-BGCyan,.-Color-BrightGreen-BGCyan,.-Color-BrightMagenta-BGCyan,.-Color-BrightRed-BGCyan,.-Color-BrightWhite-BGCyan,.-Color-BrightYellow-BGCyan,.-Color-Cyan-BGCyan,.-Color-Green-BGCyan,.-Color-Magenta-BGCyan,.-Color-Red-BGCyan,.-Color-White-BGCyan,.-Color-Yellow-BGCyan{background-color:var(--ansi-cyan)}.-Color-BGWhite,.-Color-Black-BGWhite,.-Color-Blue-BGWhite,.-Color-Bold-BGWhite,.-Color-Bold-Black-BGWhite,.-Color-Bold-Blue-BGWhite,.-Color-Bold-Cyan-BGWhite,.-Color-Bold-Green-BGWhite,.-Color-Bold-Magenta-BGWhite,.-Color-Bold-Red-BGWhite,.-Color-Bold-White-BGWhite,.-Color-Bold-Yellow-BGWhite,.-Color-BrightBGWhite,.-Color-BrightBlack-BGWhite,.-Color-BrightBlue-BGWhite,.-Color-BrightCyan-BGWhite,.-Color-BrightGreen-BGWhite,.-Color-BrightMagenta-BGWhite,.-Color-BrightRed-BGWhite,.-Color-BrightWhite-BGWhite,.-Color-BrightYellow-BGWhite,.-Color-Cyan-BGWhite,.-Color-Green-BGWhite,.-Color-Magenta-BGWhite,.-Color-Red-BGWhite,.-Color-White-BGWhite,.-Color-Yellow-BGWhite{background-color:var(--ansi-white)}.-Color-Black,.-Color-Black-BGBlack,.-Color-Black-BGGreen,.-Color-Blue-BGBlue,.-Color-Bold-Black,.-Color-Bold-Black-BGBlack,.-Color-Bold-Blue-BGBlue,.-Color-Bold-Red-BGRed,.-Color-BrightBlack,.-Color-BrightBlack-BGBlack,.-Color-BrightBlue-BGBlue,.-Color-BrightRed-BGRed,.-Color-Red-BGRed{text-shadow:0 0 1px var(--ansi-white)}.-Color-Bold-Cyan-BGCyan,.-Color-Bold-Green-BGGreen,.-Color-Bold-Magenta-BGMagenta,.-Color-Bold-White,.-Color-Bold-Yellow-BGYellow,.-Color-BrightCyan-BGCyan,.-Color-BrightGreen-BGGreen,.-Color-BrightMagenta-BGMagenta,.-Color-BrightWhite,.-Color-BrightYellow-BGYellow,.-Color-Cyan-BGCyan,.-Color-Cyan-BGGreen,.-Color-Green-BGCyan,.-Color-Green-BGGreen,.-Color-Magenta-BGMagenta,.-Color-White,.-Color-White-BGWhite,.-Color-Yellow-BGYellow{text-shadow:0 0 1px var(--ansi-black)}html.glightbox-open{height:100%;overflow:initial}html .gslide .gslide-description,html .gslide .gslide-image img{background:var(--md-default-bg-color)}html .gslide .gslide-description{-webkit-user-select:text;user-select:text}html .gslide .gslide-title{color:var(--md-default-fg-color);font-size:.8rem;margin-bottom:.4rem;margin-top:0}html .gslide .gslide-desc{color:var(--md-default-fg-color--light);font-size:.7rem}: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;font-size:.85em;padding:.8203125em 0 .8203125em 1.25em;-webkit-user-select:none;user-select:none}.highlighttable tr:first-child>.linenos{border-top-left-radius:.4rem}.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 .highlighttable>tbody>tr:nth-child(2)>.code>div>pre>code{border-top-right-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/v5.1/assets/stylesheets/modern/palette.dfe2e883.min.css b/v5.1/assets/stylesheets/modern/palette.dfe2e883.min.css new file mode 100644 index 0000000..d58a561 --- /dev/null +++ b/v5.1/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/v5.1/examples/pagination-search/index.html b/v5.1/examples/pagination-search/index.html new file mode 100644 index 0000000..701123f --- /dev/null +++ b/v5.1/examples/pagination-search/index.html @@ -0,0 +1,2306 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Pagination & search - FastAPI Toolsets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+ + + + + + + + +
+ +
+ + + +
+
+ + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + +

Pagination & search

+

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
+
+from fastapi_toolsets.db import Database
+
+DATABASE_URL = "postgresql+asyncpg://postgres:postgres@localhost:5432/postgres"
+
+db = Database(url=DATABASE_URL)
+
+get_db = db
+
+SessionDep = Annotated[AsyncSession, Depends(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 .db import db
+from .routes import router
+
+app = FastAPI()
+db.install(app=app)
+init_exceptions_handlers(app=app)
+app.include_router(router=router)
+
+

Routes

+
routes.py:1:16
from typing import Annotated
+
+from fastapi import APIRouter, Depends
+
+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")
+
+

Offset pagination

+

Best for admin panels or any UI that needs a total item count and numbered pages.

+
routes.py:19:37
@router.get("/offset")
+async def list_articles_offset(
+    session: SessionDep,
+    params: Annotated[
+        dict,
+        Depends(
+            ArticleCrud.offset_paginate_params(
+                default_page_size=20,
+                max_page_size=100,
+                default_order_field=Article.created_at,
+            )
+        ),
+    ],
+) -> OffsetPaginatedResponse[ArticleRead]:
+    return await ArticleCrud.offset_paginate(
+        session=session,
+        **params,
+        schema=ArticleRead,
+    )
+
+

Example request

+
GET /articles/offset?page=2&items_per_page=10&search=fastapi&status=published&order_by=title&order=asc
+
+

Example response

+
{
+  "status": "SUCCESS",
+  "pagination_type": "offset",
+  "data": [
+    { "id": "3f47ac69-...", "title": "FastAPI tips", "status": "published", ... }
+  ],
+  "pagination": {
+    "total_count": 42,
+    "pages": 5,
+    "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.

+

To skip the COUNT(*) query for better performance on large tables, pass include_total=False. pagination.total_count will be null in the response, while has_more remains accurate.

+

Cursor pagination

+

Best for feeds, infinite scroll, or any high-throughput API where offset performance degrades.

+
routes.py:40:58
@router.get("/cursor")
+async def list_articles_cursor(
+    session: SessionDep,
+    params: Annotated[
+        dict,
+        Depends(
+            ArticleCrud.cursor_paginate_params(
+                default_page_size=20,
+                max_page_size=100,
+                default_order_field=Article.created_at,
+            )
+        ),
+    ],
+) -> CursorPaginatedResponse[ArticleRead]:
+    return await ArticleCrud.cursor_paginate(
+        session=session,
+        **params,
+        schema=ArticleRead,
+    )
+
+

Example request

+
GET /articles/cursor?items_per_page=10&status=published&order_by=created_at&order=desc
+
+

Example response

+
{
+  "status": "SUCCESS",
+  "pagination_type": "cursor",
+  "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.

+

Unified endpoint (both strategies)

+
+

Added in v2.3.0

+
+

paginate() lets a single endpoint support both strategies via a pagination_type query parameter. The pagination_type field in the response acts as a discriminator for frontend tooling.

+
routes.py:61:79
@router.get("/")
+async def list_articles(
+    session: SessionDep,
+    params: Annotated[
+        dict,
+        Depends(
+            ArticleCrud.paginate_params(
+                default_page_size=20,
+                max_page_size=100,
+                default_order_field=Article.created_at,
+            )
+        ),
+    ],
+) -> PaginatedResponse[ArticleRead]:
+    return await ArticleCrud.paginate(
+        session,
+        **params,
+        schema=ArticleRead,
+    )
+
+

Offset request (default)

+
GET /articles/?pagination_type=offset&page=1&items_per_page=10
+
+
{
+  "status": "SUCCESS",
+  "pagination_type": "offset",
+  "data": ["..."],
+  "pagination": { "total_count": 42, "pages": 5, "page": 1, "items_per_page": 10, "has_more": true }
+}
+
+

Cursor request

+
GET /articles/?pagination_type=cursor&items_per_page=10
+GET /articles/?pagination_type=cursor&items_per_page=10&cursor=eyJ2YWx1ZSI6...
+
+
{
+  "status": "SUCCESS",
+  "pagination_type": "cursor",
+  "data": ["..."],
+  "pagination": { "next_cursor": "eyJ2YWx1ZSI6...", "prev_cursor": null, "items_per_page": 10, "has_more": true }
+}
+
+

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],
+)
+
+ + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/v5.1/index.html b/v5.1/index.html new file mode 100644 index 0000000..7d55764 --- /dev/null +++ b/v5.1/index.html @@ -0,0 +1,1915 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + FastAPI Toolsets - 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.

+

CI +codecov +ty +uv +Ruff +Python 3.11+ +License: MIT

+
+

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, UUIDv7Mixin, CreatedAtMixin, UpdatedAtMixin, TimestampMixin).
  • +
  • Lifecycle Events: Post-commit event system (EventSession, listens_for) that dispatches async/sync callbacks for insert, update, and delete operations.
  • +
  • Standardized API Responses: Consistent response format with Response, ErrorResponse, PaginatedResponse, CursorPaginatedResponse and OffsetPaginatedResponse.
  • +
  • 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.

+ + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/v5.1/migration/v2/index.html b/v5.1/migration/v2/index.html new file mode 100644 index 0000000..2752d26 --- /dev/null +++ b/v5.1/migration/v2/index.html @@ -0,0 +1,2153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Migrating to v2.0 - FastAPI Toolsets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + +

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.

+
+
+
+
# 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.

+
+
+
+
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():

+
    +
  1. The as_response parameter is renamed to return_response.
  2. +
  3. When called without return_response=True, the method now returns None on success instead of True.
  4. +
+
+
+
+
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.

+
+
+
+
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.

+
+
+
+
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 alias removed

+

Pagination was already deprecated in v1 and is fully removed in v2, you now need to use OffsetPagination or CursorPagination.

+ + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/v5.1/migration/v3/index.html b/v5.1/migration/v3/index.html new file mode 100644 index 0000000..b82d86e --- /dev/null +++ b/v5.1/migration/v3/index.html @@ -0,0 +1,2145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Migrating to v3.0 - FastAPI Toolsets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + +

Migrating to v3.0

+

This page covers every breaking change introduced in v3.0 and the steps required to update your code.

+
+

CRUD

+

Facet keys now always use the full relationship chain

+

In v2, relationship facet fields used only the terminal column key (e.g. "name" for Role.name) and only prepended the relationship name when two facet fields shared the same column key. In v3, facet keys always include the full relationship chain joined by __, regardless of collisions.

+
+
+
+
User.status -> status
+(User.role, Role.name) -> name
+(User.role, Role.permission, Permission.name) -> name
+
+
+
+
User.status -> status
+(User.role, Role.name) -> role__name
+(User.role, Role.permission, Permission.name) -> role__permission__name
+
+
+
+
+
+

*_params dependencies consolidated into per-paginate methods

+

The six individual dependency methods (offset_params, cursor_params, paginate_params, filter_params, search_params, order_params) have been removed and replaced by three consolidated methods that bundle pagination, search, filter, and order into a single Depends() call.

+ + + + + + + + + + + + + + + + + + + + + +
RemovedReplacement
offset_params() + filter_params() + search_params() + order_params()offset_paginate_params()
cursor_params() + filter_params() + search_params() + order_params()cursor_paginate_params()
paginate_params() + filter_params() + search_params() + order_params()paginate_params()
+

Each new method accepts search, filter, and order boolean toggles (all True by default) to disable features you don't need.

+
+
+
+
from fastapi_toolsets.crud import OrderByClause
+
+
+@router.get("/offset")
+async def list_articles_offset(
+    session: SessionDep,
+    params: Annotated[dict, Depends(ArticleCrud.offset_params(default_page_size=20))],
+    filter_by: Annotated[dict, 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,
+        search=search,
+        filter_by=filter_by or None,
+        order_by=order_by,
+        schema=ArticleRead,
+    )
+
+
+
+
@router.get("/offset")
+async def list_articles_offset(
+    session: SessionDep,
+    params: Annotated[
+        dict,
+        Depends(
+            ArticleCrud.offset_paginate_params(
+                default_page_size=20,
+                default_order_field=Article.created_at,
+            )
+        ),
+    ],
+) -> OffsetPaginatedResponse[ArticleRead]:
+    return await ArticleCrud.offset_paginate(
+        session=session, **params, schema=ArticleRead
+    )
+
+
+
+
+

The same pattern applies to cursor_paginate_params() and paginate_params(). To disable a feature, pass the toggle:

+
# No search or ordering, only pagination + filtering
+ArticleCrud.offset_paginate_params(search=False, order=False)
+
+
+

Models

+

The lifecycle event system has been rewritten. Callbacks are now registered with a module-level listens_for decorator and dispatched by EventSession, replacing the mixin-based approach from v2.

+

WatchedFieldsMixin and @watch removed

+

Importing WatchedFieldsMixin or watch will raise ImportError.

+

Model method callbacks (on_create, on_delete, on_update) and the @watch decorator are replaced by:

+
    +
  1. __watched_fields__ — a plain class attribute to restrict which field changes trigger UPDATE events (replaces @watch).
  2. +
  3. @listens_for — a module-level decorator to register callbacks for one or more ModelEvent types (replaces on_create / on_delete / on_update methods).
  4. +
+
+
+
+
from fastapi_toolsets.models import WatchedFieldsMixin, watch
+
+
+@watch("status")
+class Order(Base, UUIDMixin, WatchedFieldsMixin):
+    __tablename__ = "orders"
+
+    status: Mapped[str]
+
+    async def on_create(self):
+        await notify_new_order(self.id)
+
+    async def on_update(self, changes):
+        if "status" in changes:
+            await notify_status_change(self.id, changes["status"])
+
+    async def on_delete(self):
+        await notify_order_cancelled(self.id)
+
+
+
+
from fastapi_toolsets.models import ModelEvent, UUIDMixin, listens_for
+
+
+class Order(Base, UUIDMixin):
+    __tablename__ = "orders"
+    __watched_fields__ = ("status",)
+
+    status: Mapped[str]
+
+
+@listens_for(Order, [ModelEvent.CREATE])
+async def on_order_created(order: Order, event_type: ModelEvent, changes: None):
+    await notify_new_order(order.id)
+
+
+@listens_for(Order, [ModelEvent.UPDATE])
+async def on_order_updated(order: Order, event_type: ModelEvent, changes: dict):
+    if "status" in changes:
+        await notify_status_change(order.id, changes["status"])
+
+
+@listens_for(Order, [ModelEvent.DELETE])
+async def on_order_deleted(order: Order, event_type: ModelEvent, changes: None):
+    await notify_order_cancelled(order.id)
+
+
+
+
+

EventSession now required

+

Without EventSession, lifecycle callbacks will silently stop firing.

+

Callbacks are now dispatched inside EventSession.commit() rather than via background tasks. Pass it as the session class when creating your session factory:

+
+
+
+
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
+
+engine = create_async_engine("postgresql+asyncpg://...")
+SessionLocal = async_sessionmaker(engine, expire_on_commit=False)
+
+
+
+
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
+from fastapi_toolsets.models import EventSession
+
+engine = create_async_engine("postgresql+asyncpg://...")
+SessionLocal = async_sessionmaker(engine, expire_on_commit=False, class_=EventSession)
+
+
+
+
+
+

Note

+

If you use create_db_session from fastapi_toolsets.pytest, the session already uses EventSession — no changes needed in tests.

+
+ + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/v5.1/migration/v4/index.html b/v5.1/migration/v4/index.html new file mode 100644 index 0000000..a8087d3 --- /dev/null +++ b/v5.1/migration/v4/index.html @@ -0,0 +1,1894 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Migrating to v4.0 - FastAPI Toolsets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+ + + + + + + + +
+ +
+ + +
+ + + + +
+
+
+ + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + +

Migrating to v4.0

+

This page covers every breaking change introduced in v4.0 and the steps required to update your code.

+
+

Database

+

lock_tables now takes a session_maker instead of a session

+

The first argument of lock_tables changed from an AsyncSession instance to an async_sessionmaker. +The function creates and manages its own dedicated session internally, yielding it to the caller.

+
+
+
+
from fastapi_toolsets.db import lock_tables, LockMode
+
+async with lock_tables(session=session, tables=[User, Account]):
+    user = await UserCrud.get(session, [User.id == 1])
+    user.balance += 100
+
+# With a custom lock mode
+async with lock_tables(session=session, tables=[Order], mode=LockMode.EXCLUSIVE):
+    await process_order(session, order_id)
+
+
+
+
from fastapi_toolsets.db import lock_tables, LockMode
+
+async with lock_tables(session_maker=session_maker, tables=[User, Account]) as session:
+    user = await UserCrud.get(session, [User.id == 1])
+    user.balance += 100
+
+# With a custom lock mode
+async with lock_tables(
+    session_maker=session_maker, tables=[Order], mode=LockMode.EXCLUSIVE
+) as session:
+    await process_order(session, order_id)
+
+
+
+
+ + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/v5.1/migration/v5/index.html b/v5.1/migration/v5/index.html new file mode 100644 index 0000000..69a972f --- /dev/null +++ b/v5.1/migration/v5/index.html @@ -0,0 +1,2185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Migrating to v5.0 - FastAPI Toolsets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + +

Migrating to v5.0

+

This page covers every breaking change introduced in v5.0 and the steps required to update your code.

+
+

Database

+

db.py is now the db/ package, built around one object, Database, that owns the engine and sessionmaker. The free functions that took a session_maker you built and passed around yourself are gone from request-handling code; Database builds the sessionmaker for you.

+

create_db_dependency / create_db_context removed in favor of Database

+

Build one Database with your URL (or an existing engine=), then use the instance directly as the FastAPI dependency, and db.session() for sessions outside request handlers.

+
+
+
+
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
+from fastapi_toolsets.db import create_db_dependency, create_db_context
+
+engine = create_async_engine("postgresql+asyncpg://...")
+SessionLocal = async_sessionmaker(engine, expire_on_commit=False)
+
+get_db = create_db_dependency(session_maker=SessionLocal)
+get_db_context = create_db_context(session_maker=SessionLocal)
+
+
+@app.get("/users")
+async def list_users(session: AsyncSession = Depends(get_db)): ...
+
+
+async def seed():
+    async with get_db_context() as session:
+        ...
+
+
+
+
from fastapi_toolsets.db import Database
+
+db = Database(url="postgresql+asyncpg://...")
+
+
+@app.get("/users")
+async def list_users(session: AsyncSession = Depends(db)): ...
+
+
+async def seed():
+    async with db.session() as session:
+        ...
+
+
+
+
+

Call db.install(app) to also commit before the response is sent (instead of in dependency teardown) and to dispose the engine on shutdown. See the db module docs.

+

get_transaction renamed to transaction

+

Same behavior (savepoint when already in a transaction, new transaction otherwise), new name, same import path.

+
+
+
+
from fastapi_toolsets.db import get_transaction
+
+async with get_transaction(session=session):
+    session.add(model)
+
+
+
+
from fastapi_toolsets.db import transaction
+
+async with transaction(session=session):
+    session.add(model)
+
+
+
+
+

If you have a Database instance, db.begin() opens a session already inside a transaction:

+
async with db.begin() as session:
+    session.add(User(name="ada"))
+
+

lock_tables is now also a Database method

+

The free lock_tables(session_maker, tables, ...) function still exists for callers who manage their own session factory, but prefer db.lock_tables(tables, ...), which drops the session_maker argument:

+
+
+
+
from fastapi_toolsets.db import lock_tables, LockMode
+
+async with lock_tables(
+    session_maker=session_maker, tables=[Order], mode=LockMode.EXCLUSIVE
+) as session:
+    await process_order(session, order_id)
+
+
+
+
from fastapi_toolsets.db import LockMode
+
+async with db.lock_tables(tables=[Order], mode=LockMode.EXCLUSIVE) as session:
+    await process_order(session, order_id)
+
+
+
+
+

create_database and cleanup_tables moved to fastapi_toolsets.db.testing

+
+
+
+
from fastapi_toolsets.db import create_database, cleanup_tables
+
+
+
+
from fastapi_toolsets.db.testing import create_database, cleanup_tables
+
+
+
+
+

Fixtures

+

get_obj_by_attr / get_field_by_attr are now FixtureRegistry methods

+

Both also change their first argument: instead of the fixture function, pass the fixture's registered name and let the registry look it up.

+
+
+
+
from fastapi_toolsets.fixtures import get_obj_by_attr, get_field_by_attr
+
+
+@fixtures.register(depends_on=["roles"])
+def users():
+    admin_role = get_obj_by_attr(fixtures=roles, attr_name="name", value="admin")
+    admin_role_id = get_field_by_attr(fixtures=roles, attr_name="name", value="admin")
+    return [User(id=1, username="alice", role_id=admin_role.id)]
+
+
+
+
@fixtures.register(depends_on=["roles"])
+def users():
+    admin_role = fixtures.obj(name="roles", attr_name="name", value="admin")
+    admin_role_id = fixtures.field(name="roles", attr_name="name", value="admin")
+    return [User(id=1, username="alice", role_id=admin_role.id)]
+
+
+
+
+

Loading/listing by context now always includes Context.BASE

+

FixtureRegistry.get_variants/get_by_context, load_fixtures_by_context, and the fixtures list/fixtures load CLI commands now implicitly union every queried context with Context.BASE. In v4, requesting a single context (e.g. "testing") returned only fixtures tagged with that context; in v5 it also returns every Context.BASE-tagged fixture.

+

If you relied on strict exclusion of base fixtures, move them out of Context.BASE into their own context.

+

load_fixtures / load_fixtures_by_context now return reloaded instances

+

Loaded objects are re-queried from the database after insert, with all relationships eager-loaded, instead of returning the exact objects your fixture function constructed. Code that compares returned objects by identity to the fixture function's output, or that expected relationships to remain unloaded, should be updated — and expect one extra query per model type per load.

+

Security

+

The security module has been removed and moved to a dedicated python package: fastapi-multiauth.

+

Run uv add fastapi-multiauth and replace from fastapi_toolsets.security import ... with from fastapi_multiauth import ....

+ + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/v5.1/module/cli/index.html b/v5.1/module/cli/index.html new file mode 100644 index 0000000..2ad2ff7 --- /dev/null +++ b/v5.1/module/cli/index.html @@ -0,0 +1,2042 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CLI - FastAPI Toolsets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+ + + + + + + + +
+ +
+ + +
+ + + + +
+
+
+ + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + +

CLI

+

Typer-based command-line interface for managing your FastAPI application, with built-in fixture commands integration.

+

Installation

+
+
+
+
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]
+custom_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.                                           │
+╰──────────────────────────────────────────────────────────────────────────────────╯
+
+

fixtures load

+
manager fixtures load [CONTEXTS]... [--strategy merge|insert|skip_existing] [--dry-run]
+
+

CONTEXTS defaults to Context.BASE when omitted, and can also be set via the FIXTURES_CONTEXT environment variable, handy for CI/deploy scripts that shouldn't need an explicit argument per environment:

+
FIXTURES_CONTEXT=testing manager fixtures load
+
+

An explicit CLI argument always takes precedence over the environment variable.

+

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]
+custom_cli = "myapp.cli:cli"
+
+
+

API Reference

+ + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/v5.1/module/crud/index.html b/v5.1/module/crud/index.html new file mode 100644 index 0000000..4effcfa --- /dev/null +++ b/v5.1/module/crud/index.html @@ -0,0 +1,3126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CRUD - FastAPI Toolsets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + +

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, a 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

+

Factory style

+
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. This is the most concise option for straightforward CRUD with no custom logic.

+

Subclass style

+
+

Added in v2.3.0

+
+
from fastapi_toolsets.crud.factory import AsyncCrud
+from myapp.models import User
+
+
+class UserCrud(AsyncCrud[User]):
+    model = User
+    searchable_fields = [User.username, User.email]
+    default_load_options = [selectinload(User.role)]
+
+

Subclassing AsyncCrud directly is the preferred style when you need to add custom methods or when the configuration is complex enough to benefit from a named class body.

+

Adding custom methods

+
class UserCrud(AsyncCrud[User]):
+    model = User
+
+    @classmethod
+    async def get_active(cls, session: AsyncSession) -> list[User]:
+        return await cls.get_multi(session, filters=[User.is_active == True])
+
+

Sharing a custom base across multiple models

+

Define a generic base class with the shared methods, then subclass it for each model:

+
from typing import Generic, TypeVar
+from sqlalchemy.ext.asyncio import AsyncSession
+from sqlalchemy.orm import DeclarativeBase
+from fastapi_toolsets.crud.factory import AsyncCrud
+
+T = TypeVar("T", bound=DeclarativeBase)
+
+
+class AuditedCrud(AsyncCrud[T], Generic[T]):
+    """Base CRUD with custom function"""
+
+    @classmethod
+    async def get_active(cls, session: AsyncSession):
+        return await cls.get_multi(session, filters=[cls.model.is_active == True])
+
+
+class UserCrud(AuditedCrud[User]):
+    model = User
+    searchable_fields = [User.username, User.email]
+
+

You can also use the factory shorthand with the same base by passing base_class:

+
UserCrud = CrudFactory(User, base_class=AuditedCrud)
+
+

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:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
MethodNot foundMultiple results
getraises NotFoundErrorraises MultipleResultsFound
get_or_nonereturns Noneraises MultipleResultsFound
firstreturns Nonereturns 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])
+
+

Row locking

+

get, get_or_none, first, get_multi, and update all accept a with_for_update parameter that appends a FOR UPDATE clause to the underlying SELECT, preventing concurrent transactions from modifying the matched rows until the current transaction commits.

+ + + + + + + + + + + + + + + + + + + + + + + + + +
ValueSQL clause
False (default)no locking
TrueFOR UPDATE
"nowait"FOR UPDATE NOWAIT
"skip_locked"FOR UPDATE SKIP LOCKED
+
# Lock before reading — typical read-modify-write pattern
+user = await UserCrud.get(session, [User.id == user_id], with_for_update=True)
+
+# Raise immediately if another transaction holds the lock
+user = await UserCrud.get(session, [User.id == user_id], with_for_update="nowait")
+
+# Skip rows already locked by another transaction (e.g. job queues)
+rows = await JobCrud.get_multi(
+    session, filters=[Job.status == "pending"], with_for_update="skip_locked"
+)
+
+# Lock atomically as part of update (prevents race between SELECT and UPDATE)
+user = await UserCrud.update(
+    session, UserUpdate(credits=10), [User.id == user_id], with_for_update=True
+)
+
+
+

Warning

+

with_for_update requires an open transaction. Wrap your call in async with session.begin() or use the transaction helper if you are not already inside one.

+
+
+

Note

+

NOWAIT raises sqlalchemy.exc.OperationalError immediately if the row is locked rather than waiting.

+
+

Pagination

+
+

Added in v1.1 (only offset_pagination via paginate if <v1.1)

+
+

Three pagination methods are available. All return a typed response whose pagination_type field tells clients which strategy was used.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
offset_paginatecursor_paginatepaginate
Return typeOffsetPaginatedResponseCursorPaginatedResponseeither, based on pagination_type param
Total countYesNo/
Jump to arbitrary pageYesNo/
Performance on deep pagesDegradesConstant/
Stable under concurrent insertsNoYes/
Use caseAdmin panels, numbered paginationFeeds, APIs, infinite scrollsingle endpoint, both strategies
+

Offset pagination

+
from typing import Annotated
+from fastapi import Depends
+
+
+@router.get("")
+async def get_users(
+    session: SessionDep,
+    params: Annotated[dict, Depends(UserCrud.offset_paginate_params())],
+) -> OffsetPaginatedResponse[UserRead]:
+    return await UserCrud.offset_paginate(session=session, **params, schema=UserRead)
+
+

The offset_paginate method returns an OffsetPaginatedResponse:

+
{
+  "status": "SUCCESS",
+  "pagination_type": "offset",
+  "data": ["..."],
+  "pagination": {
+    "total_count": 100,
+    "pages": 5,
+    "page": 1,
+    "items_per_page": 20,
+    "has_more": true
+  }
+}
+
+

Skipping the COUNT query

+
+

Added in v2.4.1

+
+

By default offset_paginate runs two queries: one for the page items and one COUNT(*) for total_count. On large tables the COUNT can be expensive. Pass include_total=False to offset_paginate_params() to skip it:

+
@router.get("")
+async def get_users(
+    session: SessionDep,
+    params: Annotated[
+        dict, Depends(UserCrud.offset_paginate_params(include_total=False))
+    ],
+) -> OffsetPaginatedResponse[UserRead]:
+    return await UserCrud.offset_paginate(session=session, **params, schema=UserRead)
+
+

Cursor pagination

+
@router.get("")
+async def list_users(
+    session: SessionDep,
+    params: Annotated[dict, Depends(UserCrud.cursor_paginate_params())],
+) -> CursorPaginatedResponse[UserRead]:
+    return await UserCrud.cursor_paginate(session=session, **params, schema=UserRead)
+
+

The cursor_paginate method returns a CursorPaginatedResponse:

+
{
+  "status": "SUCCESS",
+  "pagination_type": "cursor",
+  "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 URL-safe base64-encoded (no padding) 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 typePython type
Integer, BigInteger, SmallIntegerint
Uuiduuid.UUID
DateTimedatetime.datetime
Datedatetime.date
Float, Numericdecimal.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)
+
+

Unified endpoint (both strategies)

+
+

Added in v2.3.0

+
+

paginate() dispatches to offset_paginate or cursor_paginate based on a pagination_type query parameter, letting you expose one endpoint that supports both strategies. The pagination_type field in the response tells clients which strategy was used, enabling frontend discriminated-union typing.

+
from fastapi_toolsets.schemas import PaginatedResponse
+
+
+@router.get("")
+async def list_users(
+    session: SessionDep,
+    params: Annotated[dict, Depends(UserCrud.paginate_params())],
+) -> PaginatedResponse[UserRead]:
+    return await UserCrud.paginate(session, **params, schema=UserRead)
+
+
GET /users?pagination_type=offset&page=2&items_per_page=10
+GET /users?pagination_type=cursor&cursor=eyJ2YWx1ZSI6...&items_per_page=10
+
+ +

Two search strategies are available, both compatible with offset_paginate and cursor_paginate.

+ + + + + + + + + + + + + + + + + + + + + + + + + +
Full-text searchFaceted search
InputFree-text stringExact column values
Relationship supportYesYes
Use caseSearch barsFilter dropdowns
+
+

You can use both search strategies in the same endpoint!

+
+ +
+

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],
+)
+
+

Or via the dependency to narrow which fields are exposed as query parameters:

+
params = UserCrud.offset_paginate_params(search_fields=[Post.title])
+
+

This allows searching with both offset_paginate and cursor_paginate:

+
@router.get("")
+async def get_users(
+    session: SessionDep,
+    params: Annotated[dict, Depends(UserCrud.offset_paginate_params())],
+) -> OffsetPaginatedResponse[UserRead]:
+    return await UserCrud.offset_paginate(session=session, **params, schema=UserRead)
+
+
@router.get("")
+async def get_users(
+    session: SessionDep,
+    params: Annotated[dict, Depends(UserCrud.cursor_paginate_params())],
+) -> CursorPaginatedResponse[UserRead]:
+    return await UserCrud.cursor_paginate(session=session, **params, schema=UserRead)
+
+

The dependency adds two query parameters to the endpoint:

+ + + + + + + + + + + + + + + + + +
ParameterType
searchstr \| null
search_columnstr \| null
+
GET /posts?search=hello                        → search all configured columns
+GET /posts?search=hello&search_column=title    → search only Post.title
+
+

The available search column keys are returned in the search_columns field of PaginatedResponse. Use them to populate a column picker in the UI, or to validate search_column values on the client side:

+
{
+  "status": "SUCCESS",
+  "data": ["..."],
+  "pagination": { "..." },
+  "search_columns": ["content", "author__username", "title"]
+}
+
+
+

Key format uses __ as a separator for relationship chains.

+

A direct column Post.title produces "title". A relationship tuple (Post.author, User.username) produces "author__username". An unknown search_column value raises InvalidSearchColumnError (HTTP 422).

+
+ +
+

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. Relationship traversal is supported via tuples, using the same syntax as searchable_fields:

+
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],
+)
+
+

Or via the dependency to narrow which fields are exposed as query parameters:

+
params = UserCrud.offset_paginate_params(facet_fields=[User.country])
+
+

Facet filtering is built into the consolidated params dependencies. When filter=True (the default), each facet field is exposed as a query parameter and values are collected into filter_by automatically:

+
from typing import Annotated
+
+from fastapi import Depends
+
+
+@router.get("", response_model_exclude_none=True)
+async def list_users(
+    session: SessionDep,
+    params: Annotated[dict, Depends(UserCrud.offset_paginate_params())],
+) -> OffsetPaginatedResponse[UserRead]:
+    return await UserCrud.offset_paginate(session=session, **params, schema=UserRead)
+
+
@router.get("", response_model_exclude_none=True)
+async def list_users(
+    session: SessionDep,
+    params: Annotated[dict, Depends(UserCrud.cursor_paginate_params())],
+) -> CursorPaginatedResponse[UserRead]:
+    return await UserCrud.cursor_paginate(session=session, **params, schema=UserRead)
+
+

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__name=admin&role__name=editor → filter_by={"role__name": ["admin", "editor"]}  (IN clause)
+
+

filter_by and filters can be combined — both are applied with AND logic.

+

The distinct values for each facet field are returned in the filter_attributes field of PaginatedResponse. Use them to populate filter dropdowns in the UI, or to validate filter_by keys on the client side:

+
{
+  "status": "SUCCESS",
+  "data": ["..."],
+  "pagination": { "..." },
+  "filter_attributes": {
+    "status": ["active", "inactive"],
+    "country": ["DE", "FR", "US"],
+    "role__name": ["admin", "editor", "viewer"]
+  }
+}
+
+
+

Key format uses __ as a separator for relationship chains.

+

A direct column User.status produces "status". A relationship tuple (User.role, Role.name) produces "role__name". A deeper chain (User.role, Role.permission, Permission.name) produces "role__permission__name". An unknown filter_by key raises InvalidFacetFilterError (HTTP 422).

+
+

Skipping facet queries

+
+

Added in v5.1.0

+
+

Facet values only change with the filters, not with the page. Pass include_facets=False to offset_paginate_params() / cursor_paginate_params() on pages 2..N to skip the facet queries entirely (filter_attributes will be None):

+
params: Annotated[dict, Depends(UserCrud.offset_paginate_params(include_facets=False))]
+
+

Sorting

+
+

Added in v1.3

+
+

Declare order_fields on the CRUD class. Relationship traversal is supported via tuples, using the same syntax as searchable_fields and facet_fields:

+
UserCrud = CrudFactory(
+    model=User,
+    order_fields=[
+        User.name,
+        User.created_at,
+        (User.role, Role.name),  # sort by a related model column
+    ],
+)
+
+

You can override order_fields per call:

+
result = await UserCrud.offset_paginate(
+    session=session,
+    order_fields=[User.name],
+)
+
+

Or via the dependency to narrow which fields are exposed as query parameters:

+
params = UserCrud.offset_paginate_params(order_fields=[User.name])
+
+

Sorting is built into the consolidated params dependencies. When order=True (the default), order_by and order query parameters are exposed and resolved into an OrderByClause automatically:

+
from typing import Annotated
+
+from fastapi import Depends
+
+
+@router.get("")
+async def list_users(
+    session: SessionDep,
+    params: Annotated[dict, Depends(UserCrud.offset_paginate_params())],
+) -> OffsetPaginatedResponse[UserRead]:
+    return await UserCrud.offset_paginate(session=session, **params, schema=UserRead)
+
+
@router.get("")
+async def list_users(
+    session: SessionDep,
+    params: Annotated[dict, Depends(UserCrud.cursor_paginate_params())],
+) -> CursorPaginatedResponse[UserRead]:
+    return await UserCrud.cursor_paginate(session=session, **params, schema=UserRead)
+
+

The dependency adds two query parameters to the endpoint:

+ + + + + + + + + + + + + + + + + +
ParameterType
order_bystr \| null
orderasc or desc
+
GET /users?order_by=name&order=asc         → ORDER BY users.name ASC
+GET /users?order_by=role__name&order=desc  → LEFT JOIN roles ON ... ORDER BY roles.name DESC
+
+
+

Relationship tuples are joined automatically.

+

When a relation field is selected, the related table is LEFT OUTER JOINed automatically. An unknown order_by value raises InvalidOrderFieldError (HTTP 422).

+
+

The available sort keys are returned in the order_columns field of PaginatedResponse. Use them to populate a sort picker in the UI, or to validate order_by values on the client side:

+
{
+  "status": "SUCCESS",
+  "data": ["..."],
+  "pagination": { "..." },
+  "order_columns": ["created_at", "name", "role__name"]
+}
+
+
+

Key format uses __ as a separator for relationship chains.

+

A direct column User.name produces "name". A relationship tuple (User.role, Role.name) produces "role__name".

+
+

Relationship loading

+
+

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

+
+

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):
+    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,
+    params: Annotated[dict, Depends(crud.UserCrud.offset_paginate_params())],
+) -> OffsetPaginatedResponse[UserRead]:
+    return await crud.UserCrud.offset_paginate(
+        session=session, **params, schema=UserRead
+    )
+
+

The schema must have from_attributes=True (or inherit from PydanticBase) so it can be built from SQLAlchemy model instances.

+
+

API Reference

+ + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/v5.1/module/db/index.html b/v5.1/module/db/index.html new file mode 100644 index 0000000..1120e28 --- /dev/null +++ b/v5.1/module/db/index.html @@ -0,0 +1,2360 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DB - FastAPI Toolsets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + +

DB

+

SQLAlchemy async session management with transactions, table locking, advisory locking, and row-change polling.

+
+

Info

+

This module has been coded and tested to be compatible with PostgreSQL only.

+
+

Overview

+

The db module is built around one object, Database, which owns the engine and sessionmaker and exposes the FastAPI dependency, a commit-before-response middleware, session/transaction context managers, and table locking. Free helpers cover savepoint-aware transactions, advisory locks, many-to-many association tables, and row-change polling.

+

Setup

+

Create one Database for your app. Provide a URL (the facade builds and disposes the engine) or pass an existing engine= you own (e.g. for Alembic or event.listen). The session factory is built internally with expire_on_commit=False.

+
from fastapi import Depends, FastAPI
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from fastapi_toolsets.db import Database
+
+db = Database("postgresql+asyncpg://postgres:postgres@localhost/app")
+
+app = FastAPI()
+db.install(app)  # commit middleware + engine disposal on shutdown
+
+
+@app.get("/users")
+async def list_users(session: AsyncSession = Depends(db)): ...
+
+

The Database instance is the dependency: use it directly as Depends(db). The whole request runs as a single transaction (CRUD writes use savepoints under it).

+

The URL may be a plain string or a Pydantic PostgresDsn. In URL mode you can tune the engine: pass connect_args for DBAPI-level options and any other keyword for create_async_engine (e.g. pool_size, echo, pool_pre_ping):

+
from pydantic_settings import BaseSettings
+from pydantic import PostgresDsn
+
+
+class Settings(BaseSettings):
+    database_url: PostgresDsn
+
+
+settings = Settings()
+
+db = Database(
+    settings.database_url,
+    pool_size=20,
+    pool_pre_ping=True,
+    connect_args={"server_settings": {"application_name": "myapp"}},
+)
+
+

Committing before the response

+

db.install(app) adds a middleware that commits the request's session when the response starts, after the endpoint returns and before the body is sent. With the middleware installed, the dependency does not commit again.

+

The request is committed as a single transaction:

+
    +
  • Read-after-write: a follow-up request sees the write.
  • +
  • Atomicity: multi-write endpoints roll back as a unit on failure.
  • +
  • Errors roll back: on a raised exception the session rolls back and nothing is committed.
  • +
+

Without install, the session commits in the dependency teardown, which runs after the response has been sent.

+
+

Streaming / SSE endpoints

+

For a StreamingResponse / EventSourceResponse, the commit fires at the start of the stream. A stream that writes must open a short-lived session per write with db.session(); the start-time commit will not flush writes made later during the stream.

+
+

Lifespan

+

db.install(app) disposes the engine on shutdown, composing around your own lifespan:

+
from contextlib import asynccontextmanager
+
+
+@asynccontextmanager
+async def lifespan(app):
+    await warm_cache()  # your startup
+    yield
+    await flush_metrics()  # your shutdown
+
+
+app = FastAPI(lifespan=lifespan)
+db.install(app)  # your shutdown runs first, then the engine is disposed
+
+

If you have no lifespan of your own, db.lifespan works standalone as FastAPI(lifespan=db.lifespan). Engine disposal is idempotent and is a no-op when you passed your own engine=.

+

Session context manager

+

Use db.session() for sessions outside request handlers (e.g. background tasks, CLI commands). It commits on clean exit and rolls back on exception:

+
async def seed():
+    async with db.session() as session:
+        ...
+
+

Transactions

+

transaction opens a transaction on a session, using a savepoint when one is already open so it nests safely:

+
from fastapi_toolsets.db import transaction
+
+
+async def create_user_with_role(session):
+    async with transaction(session):
+        ...
+        async with transaction(session):  # uses a savepoint
+            ...
+
+

When you have a Database, db.begin() opens a session already inside a transaction:

+
async with db.begin() as session:
+    session.add(User(name="ada"))  # commits on exit, rolls back on exception
+
+

Table locking

+

db.lock_tables acquires PostgreSQL table-level locks for a critical section. It opens a dedicated session internally and releases the lock when the context exits:

+
from fastapi_toolsets.db import LockMode
+
+async with db.lock_tables([User], mode=LockMode.EXCLUSIVE) as session:
+    # 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.

+

Pass timeout to limit how long the lock waits. On timeout, a LockTimeoutError is raised instead of a raw database error:

+
async with db.lock_tables([Order], timeout="2s") as session:
+    ...
+
+

Advisory locking

+

advisory_lock acquires a PostgreSQL session-level advisory lock on a session you provide. The lock is released when the context exits:

+
from fastapi_toolsets.db import advisory_lock
+
+# Blocking exclusive lock: waits until the lock is free
+async with advisory_lock(session=session, key=42):
+    ...
+
+# Non-blocking: yields False immediately if already held
+async with advisory_lock(session=session, key=42, nowait=True) as acquired:
+    if not acquired:
+        raise HTTPException(409, "Resource is locked")
+
+# Blocking with a timeout: raises LockTimeoutError if not acquired in time
+async with advisory_lock(session=session, key=42, timeout="5s"):
+    ...
+
+# Shared lock: multiple readers allowed simultaneously, blocks exclusive writers
+async with advisory_lock(session=session, key=42, shared=True):
+    ...
+
+# Two-integer key for namespacing (e.g. lock_type + resource_id)
+async with advisory_lock(session=session, key=(1, user_id)):
+    ...
+
+
+

Note

+

Advisory locks use PostgreSQL session-level functions (pg_advisory_lock / pg_advisory_unlock). The lock is tied to the database connection, not the SQLAlchemy transaction, so it is released when the context exits even if the surrounding transaction is still open.

+
+

Row-change polling

+

wait_for_row_change polls a row until a specific column changes value:

+
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=["status"],
+    interval=1.0,
+    timeout=30.0,
+)
+
+

Creating a database

+

create_database (in fastapi_toolsets.db.testing) connects to server_url and issues a CREATE DATABASE statement:

+
from fastapi_toolsets.db.testing 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, which handles drop-before, create, and drop-after.

+

Cleaning up tables

+

cleanup_tables (in fastapi_toolsets.db.testing) truncates all tables:

+
from fastapi_toolsets.db.testing import cleanup_tables
+
+
+@pytest.fixture(autouse=True)
+async def clean(db_session):
+    yield
+    await cleanup_tables(session=db_session, base=Base)
+
+

Many-to-Many helpers

+

The three m2m_* helpers modify a many-to-many association table with direct SQL, without loading the ORM collection.

+

m2m_add: insert associations

+

m2m_add inserts one or more rows into a secondary table:

+
from fastapi_toolsets.db import m2m_add
+
+async with db.lock_tables([Tag]) as session:
+    tag = await TagCrud.create(session, TagCreate(name="python"))
+    await m2m_add(session, post, Post.tags, tag)
+
+

Pass ignore_conflicts=True to skip associations that already exist:

+
await m2m_add(session, post, Post.tags, tag, ignore_conflicts=True)
+
+

m2m_remove: delete associations

+

m2m_remove deletes specific association rows. Removing a non-existent association is a no-op:

+
from fastapi_toolsets.db import m2m_remove, transaction
+
+async with transaction(session):
+    await m2m_remove(session, post, Post.tags, tag1, tag2)
+
+

m2m_set: replace the full set

+

m2m_set replaces all associations: it deletes every existing row for the owner instance then inserts the new set. Passing no related instances clears the association:

+
from fastapi_toolsets.db import m2m_set, transaction
+
+# Replace all tags
+async with transaction(session):
+    await m2m_set(session, post, Post.tags, tag_a, tag_b)
+
+# Clear all tags
+async with transaction(session):
+    await m2m_set(session, post, Post.tags)
+
+

All three helpers raise TypeError if the relationship attribute is not a Many-to-Many (i.e. has no secondary table).

+
+

API Reference

+ + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/v5.1/module/dependencies/index.html b/v5.1/module/dependencies/index.html new file mode 100644 index 0000000..e180bbf --- /dev/null +++ b/v5.1/module/dependencies/index.html @@ -0,0 +1,1932 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Dependencies - FastAPI Toolsets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+ + + + + + + + +
+ +
+ + +
+ + + + +
+
+
+ + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + +

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
+
+# Plain callable
+UserDep = PathDependency(model=User, field=User.id, session_dep=get_db)
+
+# Annotated
+SessionDep = Annotated[AsyncSession, Depends(get_db)]
+UserDep = PathDependency(model=User, field=User.id, session_dep=SessionDep)
+
+
+@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
+
+# Plain callable
+RoleDep = BodyDependency(
+    model=Role, field=Role.id, session_dep=get_db, body_field="role_id"
+)
+
+# Annotated
+SessionDep = Annotated[AsyncSession, Depends(get_db)]
+RoleDep = BodyDependency(
+    model=Role, field=Role.id, session_dep=SessionDep, 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

+ + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/v5.1/module/exceptions/index.html b/v5.1/module/exceptions/index.html new file mode 100644 index 0000000..e272ed0 --- /dev/null +++ b/v5.1/module/exceptions/index.html @@ -0,0 +1,2186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Exceptions - FastAPI Toolsets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+ + + + + + + + +
+ +
+ + + +
+
+ + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + +

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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ExceptionStatusDefault message
UnauthorizedError401Unauthorized
ForbiddenError403Forbidden
NotFoundError404Not Found
ConflictError409Conflict
NoSearchableFieldsError400No Searchable Fields
InvalidFacetFilterError400Invalid Facet Filter
InvalidOrderFieldError422Invalid Order Field
PoolExhaustedError503Service Unavailable
LockTimeoutError503Service Unavailable
+

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:

+ + + + + + + + + + + + + + + + + + + + + +
ArgumentEffect
detailOverrides both str(exc) (log output) and the message field in the response body
descOverrides the description field
dataOverrides 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]},
+        )
+
+

Intermediate base classes

+

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

+ + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/v5.1/module/fixtures/index.html b/v5.1/module/fixtures/index.html new file mode 100644 index 0000000..28c2a19 --- /dev/null +++ b/v5.1/module/fixtures/index.html @@ -0,0 +1,2290 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Fixtures - FastAPI Toolsets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + +

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, fixtures, Context.TESTING)
+
+

Directly by name with load_fixtures:

+
from fastapi_toolsets.fixtures import load_fixtures
+
+async with db_context() as session:
+    await load_fixtures(session, fixtures, "roles", "test_users")
+
+

Both functions return a dict[str, list[...]] mapping each fixture name to the list of loaded instances.

+

Contexts

+

Context is an enum with predefined values:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
ContextDescription
Context.BASECore data required in all environments
Context.TESTINGData only loaded during tests
Context.DEVELOPMENTData only loaded in development
Context.PRODUCTIONData only loaded in production
+

A fixture with no contexts defined takes Context.BASE by default.

+

Context.BASE fixtures are always included alongside whatever context you load or list — there's no way to load a non-base context in isolation:

+
# also loads any Context.BASE fixtures, even though only TESTING is requested
+await load_fixtures_by_context(session, fixtures, Context.TESTING)
+
+

Custom contexts

+

Plain strings and any Enum subclass are accepted wherever a Context enum is expected.

+
from enum import Enum
+
+
+class AppContext(str, Enum):
+    STAGING = "staging"
+    DEMO = "demo"
+
+
+@fixtures.register(contexts=[AppContext.STAGING])
+def staging_data():
+    return [Config(key="feature_x", enabled=True)]
+
+
+# loads staging_data plus any Context.BASE fixtures
+await load_fixtures_by_context(session, fixtures, AppContext.STAGING)
+
+

Default context for a registry

+

Pass contexts to FixtureRegistry to set a default for all fixtures registered in it:

+
testing_registry = FixtureRegistry(contexts=[Context.TESTING])
+
+
+@testing_registry.register  # implicitly contexts=[Context.TESTING]
+def test_orders():
+    return [Order(id=1, total=99)]
+
+

Same fixture name, multiple context variants

+

The same fixture name may be registered under different (non-overlapping) context sets. When multiple contexts are loaded together, all matching variants are merged:

+
@fixtures.register(contexts=[Context.BASE])
+def users():
+    return [User(id=1, username="admin")]
+
+
+@fixtures.register(contexts=[Context.TESTING])
+def users():
+    return [User(id=2, username="tester")]
+
+
+# loads both admin and tester (Context.BASE is included automatically)
+await load_fixtures_by_context(session, fixtures, Context.TESTING)
+
+

Registering two variants with overlapping context sets raises ValueError.

+

Load strategies

+

LoadStrategy controls how the fixture loader handles rows that already exist:

+ + + + + + + + + + + + + + + + + + + + + +
StrategyDescription
LoadStrategy.INSERTInsert only, fail on duplicates
LoadStrategy.MERGEInsert or update on conflict (default)
LoadStrategy.SKIP_EXISTINGSkip rows that already exist
+
await load_fixtures_by_context(
+    session, fixtures, Context.BASE, strategy=LoadStrategy.SKIP_EXISTING
+)
+
+

Merging registries

+

Split fixture definitions across modules and merge them:

+
from myapp.fixtures.dev import dev_fixtures
+from myapp.fixtures.prod import prod_fixtures
+
+fixtures = FixtureRegistry()
+fixtures.include_registry(registry=dev_fixtures)
+fixtures.include_registry(registry=prod_fixtures)
+
+

Fixtures with the same name are allowed as long as their context sets do not overlap. Conflicting contexts raise ValueError.

+

Looking up fixture instances

+

FixtureRegistry.obj retrieves a specific instance from a registered fixture by attribute value, looked up by name on the registry — useful when building cross-fixture depends_on relationships:

+
@fixtures.register(depends_on=["roles"])
+def users():
+    admin_role = fixtures.obj("roles", "name", "admin")
+    return [User(id=1, username="alice", role_id=admin_role.id)]
+
+

Looking the fixture up by name (instead of importing the roles function directly) means fixture modules never need to import each other, which avoids circular imports in larger projects split across multiple files — the same reason depends_on takes fixture names rather than the functions themselves. The registry passed in must be the one that actually contains the fixture by load time; with a single shared registry this is automatic, but if you merge registries with include_registry, call obj/field on the merged registry.

+

FixtureRegistry.field is shorthand for pulling a single attribute (id by default):

+
@fixtures.register(depends_on=["roles"])
+def users():
+    return [
+        User(id=1, username="alice", role_id=fixtures.field("roles", "name", "admin"))
+    ]
+
+

Both raise StopIteration if no matching instance is found, and KeyError if the fixture name isn't registered.

+

Pytest integration

+

Use register_fixtures to expose each fixture in your registry as an injectable pytest fixture named fixture_{name} by default:

+
# 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

+ + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/v5.1/module/logger/index.html b/v5.1/module/logger/index.html new file mode 100644 index 0000000..5f98c26 --- /dev/null +++ b/v5.1/module/logger/index.html @@ -0,0 +1,1904 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Logger - FastAPI Toolsets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+ + + + + + + + +
+ +
+ + +
+ + + + +
+
+
+ + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + +

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

+ + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/v5.1/module/metrics/index.html b/v5.1/module/metrics/index.html new file mode 100644 index 0000000..957fc5a --- /dev/null +++ b/v5.1/module/metrics/index.html @@ -0,0 +1,2088 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Metrics - FastAPI Toolsets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+ + + + + + + + +
+ +
+ + + +
+
+ + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + +

Metrics

+

Prometheus metrics integration with a decorator-based registry and multi-process support.

+

Installation

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

+ + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/v5.1/module/models/index.html b/v5.1/module/models/index.html new file mode 100644 index 0000000..a4ad9d8 --- /dev/null +++ b/v5.1/module/models/index.html @@ -0,0 +1,2360 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Models - FastAPI Toolsets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+ +
+ + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + +

Models

+
+

Added in v2.0

+
+

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 (clock_timestamp()), so they are also applied when inserting rows via raw SQL outside the ORM.

+

Mixins

+

UUIDMixin

+

Adds a id: UUID primary key generated server-side by PostgreSQL using gen_random_uuid(). The value is retrieved via RETURNING after insert, so it is available on the Python object immediately after flush().

+
+

Requires PostgreSQL 13+

+
+
from fastapi_toolsets.models import UUIDMixin
+
+
+class User(Base, UUIDMixin):
+    __tablename__ = "users"
+
+    username: Mapped[str]
+
+
+# id is None before flush
+user = User(username="alice")
+session.add(user)
+await session.flush()
+print(user.id)  # UUID('...')
+
+

UUIDv7Mixin

+
+

Added in v2.3

+
+

Adds a id: UUID primary key generated server-side by PostgreSQL using uuidv7(). It's a time-ordered UUID format that encodes a millisecond-precision timestamp in the most significant bits, making it naturally sortable and index-friendly.

+
+

Requires PostgreSQL 18+

+
+
from fastapi_toolsets.models import UUIDv7Mixin
+
+
+class Event(Base, UUIDv7Mixin):
+    __tablename__ = "events"
+
+    name: Mapped[str]
+
+
+# id is None before flush
+event = Event(name="user.signup")
+session.add(event)
+await session.flush()
+print(event.id)  # UUID('019...')
+
+

CreatedAtMixin

+

Adds a created_at: datetime column set to clock_timestamp() 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]
+
+

UpdatedAtMixin

+

Adds an updated_at: datetime column set to clock_timestamp() on insert and automatically updated to clock_timestamp() 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.

+
+

TimestampMixin

+

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]
+
+

Lifecycle events

+

The event system provides lifecycle callbacks that fire after commit. If the transaction rolls back, no callback fires.

+

Setup

+

Event dispatch requires EventSession. Pass it as the session class when creating your session factory:

+
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
+from fastapi_toolsets.models import EventSession
+
+engine = create_async_engine("postgresql+asyncpg://...")
+SessionLocal = async_sessionmaker(engine, expire_on_commit=False, class_=EventSession)
+
+
+

Callbacks fire on session.commit() only — not on savepoints.

+

Savepoints created by transaction or begin_nested() do not +trigger callbacks. All events accumulated across flushes are dispatched once +when the outermost commit() is called.

+
+

Events

+

Three event types are available, each corresponding to a ModelEvent value:

+ + + + + + + + + + + + + + + + + + + + + +
EventTrigger
ModelEvent.CREATEAfter INSERT commit
ModelEvent.DELETEAfter DELETE commit
ModelEvent.UPDATEAfter UPDATE commit on a watched field
+
+

Callbacks fire only for ORM-level changes. Rows updated via raw SQL (UPDATE ... SET ...) are not detected.

+
+

Watched fields

+

Set __watched_fields__ on the model to restrict which field changes trigger UPDATE events. It must be a tuple[str, ...] — any other type raises TypeError:

+ + + + + + + + + + + + + + + + + +
Class attributeUPDATE behaviour
__watched_fields__ = ("status", "role")Only fires when status or role changes
(not set)Fires when any mapped field changes
+

__watched_fields__ is inherited through the class hierarchy via normal Python MRO. A subclass can override it:

+
class Order(Base, UUIDMixin):
+    __watched_fields__ = ("status",)
+    ...
+
+
+class UrgentOrder(Order):
+    # inherits __watched_fields__ = ("status",)
+    ...
+
+
+class PriorityOrder(Order):
+    __watched_fields__ = ("priority",)
+    # overrides parent — UPDATE fires only for priority changes
+    ...
+
+

Registering handlers

+

Register handlers with the listens_for decorator. Every callback receives three arguments: the model instance, the ModelEvent that triggered it, and a changes dict (None for CREATE and DELETE):

+
from fastapi_toolsets.models import ModelEvent, UUIDMixin, listens_for
+
+
+class Order(Base, UUIDMixin):
+    __tablename__ = "orders"
+    __watched_fields__ = ("status",)
+
+    status: Mapped[str]
+
+
+@listens_for(Order, [ModelEvent.CREATE])
+async def on_order_created(order: Order, event_type: ModelEvent, changes: None):
+    await notify_new_order(order.id)
+
+
+@listens_for(Order, [ModelEvent.DELETE])
+async def on_order_deleted(order: Order, event_type: ModelEvent, changes: None):
+    await notify_order_cancelled(order.id)
+
+
+@listens_for(Order, [ModelEvent.UPDATE])
+async def on_order_updated(order: Order, event_type: ModelEvent, changes: dict):
+    if "status" in changes:
+        await notify_status_change(order.id, changes["status"])
+
+

Multiple handlers can be registered for the same model and event. Handlers registered on a parent class also fire for subclass instances.

+

A single handler can listen for multiple events at once. When event_types is omitted, the handler fires for all events:

+
@listens_for(Order, [ModelEvent.CREATE, ModelEvent.UPDATE])
+async def on_order_changed(order: Order, event_type: ModelEvent, changes: dict | None):
+    await invalidate_cache(order.id)
+
+
+@listens_for(Order)  # all events
+async def on_any_order_event(
+    order: Order, event_type: ModelEvent, changes: dict | None
+):
+    await audit_log(order.id, event_type)
+
+

Field changes format

+

The changes dict maps each watched field that changed to {"old": ..., "new": ...}. Only fields that actually changed are included. For CREATE and DELETE events, changes is None:

+
# CREATE / DELETE → changes is None
+# status changed   → {"status": {"old": "pending", "new": "shipped"}}
+# two fields changed → {"status": {...}, "assigned_to": {...}}
+
+
+

Multiple flushes in one transaction are merged: the earliest old and latest new are preserved, and on_update fires only once per commit.

+
+
+

API Reference

+ + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/v5.1/module/pytest/index.html b/v5.1/module/pytest/index.html new file mode 100644 index 0000000..59e3b3e --- /dev/null +++ b/v5.1/module/pytest/index.html @@ -0,0 +1,2065 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Pytest - FastAPI Toolsets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+ + + + + + + + +
+ +
+ + + +
+
+ + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + +

Pytest

+

Testing helpers for FastAPI applications: async HTTP client, database sessions, and parallel worker support.

+

Installation

+
+
+
+
uv add "fastapi-toolsets[pytest]"
+
+
+
+
pip install "fastapi-toolsets[pytest]"
+
+
+
+
+

Async client

+

Use create_async_client to get an httpx.AsyncClient bound to 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
+
+

Any extra keyword arguments are forwarded to httpx.AsyncClient, so you can set default headers, authentication, timeouts, and more:

+
async with create_async_client(
+    app=app,
+    headers={"X-Api-Key": "secret"},
+    timeout=10,
+) as c:
+    ...
+
+

Database sessions

+

Use create_worker_database + create_db_session to get a fully isolated AsyncSession for each test:

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

create_worker_database connects without specifying a database (asyncpg falls back to the username), so the target test database does not need to exist beforehand.

+
+

Info

+

cleanup=True truncates all tables between tests via TRUNCATE … RESTART IDENTITY CASCADE, which is faster than dropping and recreating tables.

+
+

Engine and session options

+

Pass engine_kwargs or session_kwargs to forward options to the underlying SQLAlchemy primitives:

+
async with create_db_session(
+    database_url=worker_db_url,
+    base=Base,
+    engine_kwargs={"pool_size": 5, "connect_args": {"timeout": 10}},
+    session_kwargs={"autoflush": False},
+) as session:
+    ...
+
+

Parallel testing with pytest-xdist

+

The fixtures above work with pytest-xdist out of the box. Each worker gets its own database named after the worker (e.g. gw0, gw1). Pass prefix to namespace the database (e.g. prefix="myapp"myapp_gw0).

+

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/myapp", default_test_db="test"
+)
+# → "postgresql+asyncpg://user:pass@localhost/gw0" under xdist
+# → "postgresql+asyncpg://user:pass@localhost/test" otherwise
+
+url = worker_database_url(
+    "postgresql+asyncpg://user:pass@localhost/myapp",
+    default_test_db="test",
+    prefix="myapp",
+)
+# → "postgresql+asyncpg://user:pass@localhost/myapp_gw0" under xdist
+# → "postgresql+asyncpg://user:pass@localhost/myapp_test" otherwise
+
+

Manual table cleanup

+

cleanup_tables truncates all tables in a single statement and can be called directly when you need more control:

+
from fastapi_toolsets.pytest import cleanup_tables
+
+
+@pytest.fixture(autouse=True)
+async def clean(db_session):
+    yield
+    await cleanup_tables(session=db_session, base=Base)
+
+
+

API Reference

+ + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/v5.1/module/schemas/index.html b/v5.1/module/schemas/index.html new file mode 100644 index 0000000..7729d8d --- /dev/null +++ b/v5.1/module/schemas/index.html @@ -0,0 +1,2172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Schemas - FastAPI Toolsets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + +

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

+

Response[T]

+

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")
+
+

Paginated response models

+

Three classes wrap paginated list results. Pick the one that matches your endpoint's strategy:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Classpagination typepagination_type fieldUse when
OffsetPaginatedResponse[T]OffsetPagination"offset" (fixed)endpoint always uses offset
CursorPaginatedResponse[T]CursorPagination"cursor" (fixed)endpoint always uses cursor
PaginatedResponse[T]OffsetPagination \| CursorPaginationunified endpoint supporting both strategies
+

OffsetPaginatedResponse[T]

+
+

Added in v2.3.0

+
+

Use as the return type when the endpoint always uses offset_paginate. The pagination field is guaranteed to be an OffsetPagination object; the response always includes a pagination_type: "offset" discriminator.

+
from fastapi_toolsets.schemas import OffsetPaginatedResponse
+
+
+@router.get("/users")
+async def list_users(
+    page: int = 1,
+    items_per_page: int = 20,
+) -> OffsetPaginatedResponse[UserSchema]:
+    return await UserCrud.offset_paginate(
+        session, page=page, items_per_page=items_per_page, schema=UserSchema
+    )
+
+

Response shape:

+
{
+  "status": "SUCCESS",
+  "pagination_type": "offset",
+  "data": ["..."],
+  "pagination": {
+    "total_count": 100,
+    "page": 1,
+    "items_per_page": 20,
+    "has_more": true
+  }
+}
+
+

CursorPaginatedResponse[T]

+
+

Added in v2.3.0

+
+

Use as the return type when the endpoint always uses cursor_paginate. The pagination field is guaranteed to be a CursorPagination object; the response always includes a pagination_type: "cursor" discriminator.

+
from fastapi_toolsets.schemas import CursorPaginatedResponse
+
+
+@router.get("/events")
+async def list_events(
+    cursor: str | None = None,
+    items_per_page: int = 20,
+) -> CursorPaginatedResponse[EventSchema]:
+    return await EventCrud.cursor_paginate(
+        session, cursor=cursor, items_per_page=items_per_page, schema=EventSchema
+    )
+
+

Response shape:

+
{
+  "status": "SUCCESS",
+  "pagination_type": "cursor",
+  "data": ["..."],
+  "pagination": {
+    "next_cursor": "eyJpZCI6IDQyfQ==",
+    "prev_cursor": null,
+    "items_per_page": 20,
+    "has_more": true
+  }
+}
+
+

PaginatedResponse[T]

+

Return type for endpoints that support both pagination strategies via a pagination_type query parameter (using paginate()).

+

When used as a return annotation, PaginatedResponse[T] automatically expands to Annotated[Union[CursorPaginatedResponse[T], OffsetPaginatedResponse[T]], Field(discriminator="pagination_type")], so FastAPI emits a proper oneOf + discriminator in the OpenAPI schema with no extra boilerplate:

+
from fastapi_toolsets.crud import PaginationType
+from fastapi_toolsets.schemas import PaginatedResponse
+
+
+@router.get("/users")
+async def list_users(
+    pagination_type: PaginationType = PaginationType.OFFSET,
+    page: int = 1,
+    cursor: str | None = None,
+    items_per_page: int = 20,
+) -> PaginatedResponse[UserSchema]:
+    return await UserCrud.paginate(
+        session,
+        pagination_type=pagination_type,
+        page=page,
+        cursor=cursor,
+        items_per_page=items_per_page,
+        schema=UserSchema,
+    )
+
+

Pagination metadata models

+

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.

+

ErrorResponse

+

Returned automatically by the exceptions handler.

+
+

API Reference

+ + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/v5.1/objects.inv b/v5.1/objects.inv new file mode 100644 index 0000000000000000000000000000000000000000..d67bd0e0d177ad6131861b422b79967b9de262dc GIT binary patch literal 1937 zcmV;C2X6QyAX9K?X>NERX>N99Zgg*Qc_4OWa&u{KZXhxWBOp+6Z)#;@bUGkLVRLjr zP)Q(EZ*OdKWpr~2BOq2~a&u{KZaN?^E-)@I3L_v?Xk{RBWo=<;Ze(S0Aa78b#rNMXCQiPX<{x4c-qaJO^@3)5Qgvl6#=@}y2xG% z^tK<~z-aaZ>)oO^ftJQzHWVq4l(X)yFDY4G*(NDUB!e7+IFjahXT%YQq^PWskf_3$ zWQ+)qVwMv;Lt;nsEN5kjXz{7q{kg7bE-|B@ES#EtSZU`ZyvuOb8o$e$gUC2+u#kKg zhQwvXxT10utD<4LnaDG>*#r`=i`kWi+^Am~kP{@tao(siro_>vYm`gIcRhqnM4!BC zueN8Xhf<$o(^^oKlvrN|@(vRTe9k%J=J4P>hf2F54ALJb`#){`0e_Z&c>Z?7NL|t- z4xJ0wJbGa5wP;1)4g)DfT!(CYn?FxzgDKLsFS`oNZixI1N@Vh3f98~86YFl1ejvxE zylAYCAvUG+$KxRP`AqJ@AESDcnmRhS@jMPlLQG)I1>;%Own##<`oU2tA_^2hAgagk zt_nv?57(HBn60nsTwGdK)q-C* z&sp1;WDHAZra06(HC)uiZqf*6Us%jQo4lzSO{y__(5l9y*y}58J+_&i3C@TnK#SGv z7D=>10?a`ct6|GomgR_$EVFvxMOmro01ZQ6s!kFT1|fJ2qC$bL5SD+-q;4J&1`;5I zHk<_L1)O%Kv2!4BXMlN?cjTJ~6-zO7jzv`^9Y!q6%B zcz3XJq3ykfkHoRHOD80MhSR3JEcKrZ6~$W|k8`w5~!Po&&0x*k?trvW;h1x(Q$V?+D9mt)_3Beb3Y& zkZxciN2<}}5X~ohObnAIR6q+w!XReXtk~Ul2ki5OzKt!EKK2R8w>a3|^4nE~9qW0+ z3uy=YVVUk<(UuO6pl*FV-yt}can~-7scsMM7+>K+wM$$VPOxqtexlXxbE`*Q0B=@+ zlQ?yX_wwoIScmVK>CwsyE`imNQ~3<+_TwjdD{y2b?^sQX*uM0sZV#T+vHANOs)dBe zZWz#RUtTGyZWEGm1sPXJFA#o%Beokqf#j6;psWtCxf(MQYtr~9E-ViMa@L6+LKaOYn<8k8m zf=`Km(0hYl-9|w@1nagQo{xpQs|@-F0MNQTP+UrP?T_c)&ctbo-QLDCqD%cNL&Z|+ z+}P{_gFQmIG>>kLu}ApOyznN?uCJDJjYCrMaN6zs@A=oqr~CQCOFZb)^W*L7&CBBP zAs+K(zI<6cd|x`vwKMa1 zy4qdB$2@Q4Y_&7)qmJN2AMNtzv||cOE^9ZJggR?Bx>yTcFy6ImF9tkw?M;ayV9BrO zI{lX?X_cBJ-y)6?m5HU&t6-Xwx`5#?AA*(G>fdzm_5|>h3WAX>bjkdK@~u;is%9z} z378QceZn>yHAHW}t5Mfw`he?un%2NgYoB5La~h@u$#E`b_s!qtQSts7L&fZ@4^6(u z3xNChF(^-nc(au5~0iQEMK<|PU zDOD=Ywzzv5O4f5Vj>#!&vtMbhe?MWU>7dNwbFUszh4aD+%#W^ z@BcFZD54{+BW*ucbDv^3_1Ju595v1(WMG`9Ed7CT+sT~081Vx}jnwu(PD~)}3PdH& XuThvsF{0`iONnGHJV^fof4w6S_w2qn literal 0 HcmV?d00001 diff --git a/v5.1/overrides/main.html b/v5.1/overrides/main.html new file mode 100644 index 0000000..4ee97dd --- /dev/null +++ b/v5.1/overrides/main.html @@ -0,0 +1,7 @@ +{% extends "base.html" %} {% block extrahead %} + +{{ super() }} {% endblock %} diff --git a/v5.1/reference/cli/index.html b/v5.1/reference/cli/index.html new file mode 100644 index 0000000..02572d7 --- /dev/null +++ b/v5.1/reference/cli/index.html @@ -0,0 +1,2253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + cli - FastAPI Toolsets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+ + + + + + + + +
+ +
+ + + +
+
+ + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + +

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,
+)
+
+ + +
+ + +

+ fastapi_toolsets.cli.config.import_from_string(import_path) + +

+ + +
+ +

Import an object from a dotted string path.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ import_path + + str + +
+

Import path in "module.submodule:attribute" format

+
+
+ required +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ Any + +
+

The imported attribute

+
+
+ + +

Raises:

+ + + + + + + + + + + + + +
TypeDescription
+ BadParameter + +
+

If the import path is invalid or import fails

+
+
+ + +
+ +
+ +
+ + +

+ fastapi_toolsets.cli.config.get_config_value(key, required=False) + +

+
+
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:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ key + + str + +
+

The configuration key in [tool.fastapi-toolsets].

+
+
+ required +
+ required + + bool + +
+

If True, raises an error when the key is missing.

+
+
+ False +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ Any | None + +
+

The configuration value, or None if not found and not required.

+
+
+ + +

Raises:

+ + + + + + + + + + + + + +
TypeDescription
+ BadParameter + +
+

If required=True and the key is missing.

+
+
+ + +
+ +
+ +
+ + +

+ fastapi_toolsets.cli.config.get_fixtures_registry() + +

+ + +
+ +

Import and return the fixtures registry from config.

+ + +
+ +
+ +
+ + +

+ fastapi_toolsets.cli.config.get_db_context() + +

+ + +
+ +

Import and return the db_context function from config.

+ + +
+ +
+ +
+ + +

+ fastapi_toolsets.cli.config.get_custom_cli() + +

+ + +
+ +

Import and return the custom CLI Typer instance from config.

+ + +
+ +
+ +
+ + +

+ fastapi_toolsets.cli.utils.async_command(func) + +

+ + +
+ +

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)
+
+
+ +
+ +
+ + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/v5.1/reference/crud/index.html b/v5.1/reference/crud/index.html new file mode 100644 index 0000000..a1b1f56 --- /dev/null +++ b/v5.1/reference/crud/index.html @@ -0,0 +1,6655 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + crud - FastAPI Toolsets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + +

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,
+)
+
+ + +
+ + + +

+ fastapi_toolsets.crud.factory.AsyncCrud + + +

+ + +
+

+ Bases: Generic[ModelType]

+ + + +

Generic async CRUD operations for SQLAlchemy models.

+

Subclass this and set the model class variable, or use CrudFactory.

+ + + + + + + + + + + +
+ + + + + + + + + + +
+ + +

+ count(session, filters=None, *, joins=None, outer_join=False) + + + async + classmethod + + +

+ + +
+ +

Count records matching the filters.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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:

+ + + + + + + + + + + + + +
TypeDescription
+ int + +
+

Number of matching records

+
+
+ + +
+ +
+ +
+ + +

+ create(session, obj, *, schema=None) + + + async + classmethod + + +

+
+
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:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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:

+ + + + + + + + + + + + + +
TypeDescription
+ ModelType | Response[Any] + +
+

Created model instance, or Response[schema] when schema is given.

+
+
+ + +
+ +
+ +
+ + +

+ cursor_paginate(session, *, cursor=None, filters=None, joins=None, outer_join=False, load_options=None, order_by=None, order_joins=None, items_per_page=20, search=None, search_fields=None, search_column=None, order_fields=None, facet_fields=None, include_facets=True, filter_by=None, schema) + + + async + classmethod + + +

+ + +
+ +

Get paginated results using cursor-based pagination.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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 +
+ search_column + + str | None + +
+

Restrict search to a single column key.

+
+
+ None +
+ order_fields + + Sequence[OrderFieldType] | None + +
+

Fields allowed for sorting (overrides class default).

+
+
+ None +
+ facet_fields + + Sequence[FacetFieldType] | None + +
+

Columns to compute distinct values for (overrides class default).

+
+
+ None +
+ include_facets + + bool + +
+

When False, skip facet queries entirely; +filter_attributes will be None.

+
+
+ True +
+ 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:

+ + + + + + + + + + + + + +
TypeDescription
+ CursorPaginatedResponse[Any] + +
+

PaginatedResponse with CursorPagination metadata

+
+
+ + +
+ +
+ +
+ + +

+ cursor_paginate_params(*, default_page_size=20, max_page_size=100, include_facets=True, search=True, filter=True, order=True, search_fields=None, facet_fields=None, order_fields=None, default_order_field=None, default_order='asc') + + + classmethod + + +

+ + +
+ +

Return a FastAPI dependency that collects all params for :meth:cursor_paginate.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ default_page_size + + int + +
+

Default items_per_page value.

+
+
+ 20 +
+ max_page_size + + int + +
+

Maximum items_per_page value.

+
+
+ 100 +
+ include_facets + + bool + +
+

Whether to run facet queries (not a query param).

+
+
+ True +
+ search + + bool + +
+

Enable search query parameters.

+
+
+ True +
+ filter + + bool + +
+

Enable facet filter query parameters.

+
+
+ True +
+ order + + bool + +
+

Enable order query parameters.

+
+
+ True +
+ search_fields + + Sequence[SearchFieldType] | None + +
+

Override searchable fields.

+
+
+ None +
+ facet_fields + + Sequence[FacetFieldType] | None + +
+

Override facet fields.

+
+
+ None +
+ order_fields + + Sequence[OrderFieldType] | None + +
+

Override order fields.

+
+
+ None +
+ default_order_field + + QueryableAttribute[Any] | None + +
+

Default field to order by when order_by is absent.

+
+
+ None +
+ default_order + + Literal['asc', 'desc'] + +
+

Default sort direction.

+
+
+ 'asc' +
+ + +

Returns:

+ + + + + + + + + + + + + + + + + +
Name TypeDescription
+ Callable[..., Awaitable[dict[str, Any]]] + +
+

An async dependency that resolves to a dict ready to be unpacked

+
+
into + Callable[..., Awaitable[dict[str, Any]]] + +
+

meth:cursor_paginate.

+
+
+ + +
+ +
+ +
+ + +

+ delete(session, filters, *, return_response=False) + + + async + classmethod + + +

+
+
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:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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:

+ + + + + + + + + + + + + +
TypeDescription
+ None | Response[None] + +
+

None, or Response[None] when return_response=True.

+
+
+ + +
+ +
+ +
+ + +

+ exists(session, filters, *, joins=None, outer_join=False) + + + async + classmethod + + +

+ + +
+ +

Check if a record exists.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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:

+ + + + + + + + + + + + + +
TypeDescription
+ bool + +
+

True if at least one record matches

+
+
+ + +
+ +
+ +
+ + +

+ first(session, filters=None, *, joins=None, outer_join=False, with_for_update=False, load_options=None, schema=None) + + + async + classmethod + + +

+
+
first(
+    session: AsyncSession,
+    filters: list[Any] | None = None,
+    *,
+    joins: JoinType | None = None,
+    outer_join: bool = False,
+    with_for_update: _ForUpdateMode = 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: _ForUpdateMode = False,
+    load_options: Sequence[ExecutableOption] | None = None,
+    schema: None = ...,
+) -> ModelType | None
+
+ + +
+ +

Get the first matching record, or None.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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 + + _ForUpdateMode + +
+

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:

+ + + + + + + + + + + + + + + + + +
TypeDescription
+ ModelType | Response[Any] | None + +
+

Model instance, Response[schema] when schema is given,

+
+
+ ModelType | Response[Any] | None + +
+

or None when no record matches.

+
+
+ + +
+ +
+ +
+ + +

+ get(session, filters, *, joins=None, outer_join=False, with_for_update=False, load_options=None, schema=None) + + + async + classmethod + + +

+
+
get(
+    session: AsyncSession,
+    filters: list[Any],
+    *,
+    joins: JoinType | None = None,
+    outer_join: bool = False,
+    with_for_update: _ForUpdateMode = 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: _ForUpdateMode = False,
+    load_options: Sequence[ExecutableOption] | None = None,
+    schema: None = ...,
+) -> ModelType
+
+ + +
+ +

Get exactly one record. Raises NotFoundError if not found.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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 + + _ForUpdateMode + +
+

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:

+ + + + + + + + + + + + + +
TypeDescription
+ ModelType | Response[Any] + +
+

Model instance, or Response[schema] when schema is given.

+
+
+ + +

Raises:

+ + + + + + + + + + + + + + + + + +
TypeDescription
+ NotFoundError + +
+

If no record found

+
+
+ MultipleResultsFound + +
+

If more than one record found

+
+
+ + +
+ +
+ +
+ + +

+ get_multi(session, *, filters=None, joins=None, outer_join=False, with_for_update=False, load_options=None, order_by=None, limit=None, offset=None) + + + async + classmethod + + +

+ + +
+ +

Get multiple records from the database.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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 + + _ForUpdateMode + +
+

Lock rows for update. True for plain FOR UPDATE, +"nowait" for FOR UPDATE NOWAIT, "skip_locked" for +FOR UPDATE SKIP LOCKED.

+
+
+ 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:

+ + + + + + + + + + + + + +
TypeDescription
+ Sequence[ModelType] + +
+

List of model instances

+
+
+ + +
+ +
+ +
+ + +

+ get_or_none(session, filters, *, joins=None, outer_join=False, with_for_update=False, load_options=None, schema=None) + + + async + classmethod + + +

+
+
get_or_none(
+    session: AsyncSession,
+    filters: list[Any],
+    *,
+    joins: JoinType | None = None,
+    outer_join: bool = False,
+    with_for_update: _ForUpdateMode = 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: _ForUpdateMode = 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:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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 + + _ForUpdateMode + +
+

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:

+ + + + + + + + + + + + + + + + + +
TypeDescription
+ ModelType | Response[Any] | None + +
+

Model instance, Response[schema] when schema is given,

+
+
+ ModelType | Response[Any] | None + +
+

or None when no record matches.

+
+
+ + +

Raises:

+ + + + + + + + + + + + + +
TypeDescription
+ MultipleResultsFound + +
+

If more than one record found

+
+
+ + +
+ +
+ +
+ + +

+ offset_paginate(session, *, filters=None, joins=None, outer_join=False, load_options=None, order_by=None, order_joins=None, page=1, items_per_page=20, include_total=True, search=None, search_fields=None, search_column=None, order_fields=None, facet_fields=None, include_facets=True, filter_by=None, schema) + + + async + classmethod + + +

+ + +
+ +

Get paginated results using offset-based pagination.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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 +
+ include_total + + bool + +
+

When False, skip the COUNT query; +pagination.total_count will be None.

+
+
+ True +
+ search + + str | SearchConfig | None + +
+

Search query string or SearchConfig object

+
+
+ None +
+ search_fields + + Sequence[SearchFieldType] | None + +
+

Fields to search in (overrides class default)

+
+
+ None +
+ search_column + + str | None + +
+

Restrict search to a single column key.

+
+
+ None +
+ order_fields + + Sequence[OrderFieldType] | None + +
+

Fields allowed for sorting (overrides class default).

+
+
+ None +
+ facet_fields + + Sequence[FacetFieldType] | None + +
+

Columns to compute distinct values for (overrides class default)

+
+
+ None +
+ include_facets + + bool + +
+

When False, skip facet queries entirely; +filter_attributes will be None. Useful on pages 2..N +where the facet counts were already fetched on page 1.

+
+
+ True +
+ 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:

+ + + + + + + + + + + + + +
TypeDescription
+ OffsetPaginatedResponse[Any] + +
+

PaginatedResponse with OffsetPagination metadata

+
+
+ + +
+ +
+ +
+ + +

+ offset_paginate_params(*, default_page_size=20, max_page_size=100, include_total=True, include_facets=True, search=True, filter=True, order=True, search_fields=None, facet_fields=None, order_fields=None, default_order_field=None, default_order='asc') + + + classmethod + + +

+ + +
+ +

Return a FastAPI dependency that collects all params for :meth:offset_paginate.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ default_page_size + + int + +
+

Default items_per_page value.

+
+
+ 20 +
+ max_page_size + + int + +
+

Maximum items_per_page value.

+
+
+ 100 +
+ include_total + + bool + +
+

Whether to include total count (not a query param).

+
+
+ True +
+ include_facets + + bool + +
+

Whether to run facet queries (not a query param).

+
+
+ True +
+ search + + bool + +
+

Enable search query parameters.

+
+
+ True +
+ filter + + bool + +
+

Enable facet filter query parameters.

+
+
+ True +
+ order + + bool + +
+

Enable order query parameters.

+
+
+ True +
+ search_fields + + Sequence[SearchFieldType] | None + +
+

Override searchable fields.

+
+
+ None +
+ facet_fields + + Sequence[FacetFieldType] | None + +
+

Override facet fields.

+
+
+ None +
+ order_fields + + Sequence[OrderFieldType] | None + +
+

Override order fields.

+
+
+ None +
+ default_order_field + + QueryableAttribute[Any] | None + +
+

Default field to order by when order_by is absent.

+
+
+ None +
+ default_order + + Literal['asc', 'desc'] + +
+

Default sort direction.

+
+
+ 'asc' +
+ + +

Returns:

+ + + + + + + + + + + + + + + + + +
Name TypeDescription
+ Callable[..., Awaitable[dict[str, Any]]] + +
+

An async dependency that resolves to a dict ready to be unpacked

+
+
into + Callable[..., Awaitable[dict[str, Any]]] + +
+

meth:offset_paginate.

+
+
+ + +
+ +
+ +
+ + +

+ paginate(session, *, pagination_type=PaginationType.OFFSET, filters=None, joins=None, outer_join=False, load_options=None, order_by=None, order_joins=None, page=1, cursor=None, items_per_page=20, include_total=True, search=None, search_fields=None, search_column=None, order_fields=None, facet_fields=None, include_facets=True, filter_by=None, schema) + + + async + classmethod + + +

+
+
paginate(
+    session: AsyncSession,
+    *,
+    pagination_type: Literal[PaginationType.OFFSET],
+    filters: list[Any] | None = ...,
+    joins: JoinType | None = ...,
+    outer_join: bool = ...,
+    load_options: Sequence[ExecutableOption] | None = ...,
+    order_by: OrderByClause | None = ...,
+    order_joins: list[Any] | None = ...,
+    page: int = ...,
+    cursor: str | None = ...,
+    items_per_page: int = ...,
+    include_total: bool = ...,
+    search: str | SearchConfig | None = ...,
+    search_fields: Sequence[SearchFieldType] | None = ...,
+    search_column: str | None = ...,
+    order_fields: Sequence[OrderFieldType] | None = ...,
+    facet_fields: Sequence[FacetFieldType] | None = ...,
+    include_facets: bool = ...,
+    filter_by: dict[str, Any] | BaseModel | None = ...,
+    schema: type[BaseModel],
+) -> OffsetPaginatedResponse[Any]
+
paginate(
+    session: AsyncSession,
+    *,
+    pagination_type: Literal[PaginationType.CURSOR],
+    filters: list[Any] | None = ...,
+    joins: JoinType | None = ...,
+    outer_join: bool = ...,
+    load_options: Sequence[ExecutableOption] | None = ...,
+    order_by: OrderByClause | None = ...,
+    order_joins: list[Any] | None = ...,
+    page: int = ...,
+    cursor: str | None = ...,
+    items_per_page: int = ...,
+    include_total: bool = ...,
+    search: str | SearchConfig | None = ...,
+    search_fields: Sequence[SearchFieldType] | None = ...,
+    search_column: str | None = ...,
+    order_fields: Sequence[OrderFieldType] | None = ...,
+    facet_fields: Sequence[FacetFieldType] | None = ...,
+    include_facets: bool = ...,
+    filter_by: dict[str, Any] | BaseModel | None = ...,
+    schema: type[BaseModel],
+) -> CursorPaginatedResponse[Any]
+
+ + +
+ +

Get paginated results using either offset or cursor pagination.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ session + + AsyncSession + +
+

DB async session.

+
+
+ required +
+ pagination_type + + PaginationType + +
+

Pagination strategy. Defaults to +PaginationType.OFFSET.

+
+
+ OFFSET +
+ 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 + +
+

Column or expression to order results by.

+
+
+ None +
+ page + + int + +
+

Page number (1-indexed). Only used when +pagination_type is OFFSET.

+
+
+ 1 +
+ cursor + + str | None + +
+

Cursor token from a previous +:class:.CursorPaginatedResponse. Only used when +pagination_type is CURSOR.

+
+
+ None +
+ items_per_page + + int + +
+

Number of items per page (default 20).

+
+
+ 20 +
+ include_total + + bool + +
+

When False, skip the COUNT query; +only applies when pagination_type is OFFSET.

+
+
+ True +
+ search + + str | SearchConfig | None + +
+

Search query string or :class:.SearchConfig object.

+
+
+ None +
+ search_fields + + Sequence[SearchFieldType] | None + +
+

Fields to search in (overrides class default).

+
+
+ None +
+ search_column + + str | None + +
+

Restrict search to a single column key.

+
+
+ None +
+ order_fields + + Sequence[OrderFieldType] | None + +
+

Fields allowed for sorting (overrides class default).

+
+
+ None +
+ facet_fields + + Sequence[FacetFieldType] | None + +
+

Columns to compute distinct values for (overrides +class default).

+
+
+ None +
+ include_facets + + bool + +
+

When False, skip facet queries entirely; +filter_attributes will be None.

+
+
+ True +
+ 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 +:exc:.InvalidFacetFilterError for unknown keys.

+
+
+ None +
+ schema + + type[BaseModel] + +
+

Pydantic schema to serialize each item into.

+
+
+ required +
+ + +

Returns:

+ + + + + + + + + + + + + + + + + + + + + +
TypeDescription
+ OffsetPaginatedResponse[Any] | CursorPaginatedResponse[Any] + +
+

class:.OffsetPaginatedResponse when pagination_type is

+
+
+ OffsetPaginatedResponse[Any] | CursorPaginatedResponse[Any] + +
+

OFFSET, :class:.CursorPaginatedResponse when it is

+
+
+ OffsetPaginatedResponse[Any] | CursorPaginatedResponse[Any] + +
+

CURSOR.

+
+
+ + +
+ +
+ +
+ + +

+ paginate_params(*, default_page_size=20, max_page_size=100, default_pagination_type=PaginationType.OFFSET, include_total=True, include_facets=True, search=True, filter=True, order=True, search_fields=None, facet_fields=None, order_fields=None, default_order_field=None, default_order='asc') + + + classmethod + + +

+ + +
+ +

Return a FastAPI dependency that collects all params for :meth:paginate.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ default_page_size + + int + +
+

Default items_per_page value.

+
+
+ 20 +
+ max_page_size + + int + +
+

Maximum items_per_page value.

+
+
+ 100 +
+ default_pagination_type + + PaginationType + +
+

Default pagination strategy.

+
+
+ OFFSET +
+ include_total + + bool + +
+

Whether to include total count (not a query param).

+
+
+ True +
+ include_facets + + bool + +
+

Whether to run facet queries (not a query param).

+
+
+ True +
+ search + + bool + +
+

Enable search query parameters.

+
+
+ True +
+ filter + + bool + +
+

Enable facet filter query parameters.

+
+
+ True +
+ order + + bool + +
+

Enable order query parameters.

+
+
+ True +
+ search_fields + + Sequence[SearchFieldType] | None + +
+

Override searchable fields.

+
+
+ None +
+ facet_fields + + Sequence[FacetFieldType] | None + +
+

Override facet fields.

+
+
+ None +
+ order_fields + + Sequence[OrderFieldType] | None + +
+

Override order fields.

+
+
+ None +
+ default_order_field + + QueryableAttribute[Any] | None + +
+

Default field to order by when order_by is absent.

+
+
+ None +
+ default_order + + Literal['asc', 'desc'] + +
+

Default sort direction.

+
+
+ 'asc' +
+ + +

Returns:

+ + + + + + + + + + + + + + + + + +
Name TypeDescription
+ Callable[..., Awaitable[dict[str, Any]]] + +
+

An async dependency that resolves to a dict ready to be unpacked

+
+
into + Callable[..., Awaitable[dict[str, Any]]] + +
+

meth:paginate.

+
+
+ + +
+ +
+ +
+ + +

+ update(session, obj, filters, *, exclude_unset=True, exclude_none=False, with_for_update=False, schema=None) + + + async + classmethod + + +

+
+
update(
+    session: AsyncSession,
+    obj: BaseModel,
+    filters: list[Any],
+    *,
+    exclude_unset: bool = True,
+    exclude_none: bool = False,
+    with_for_update: _ForUpdateMode = False,
+    schema: type[SchemaType],
+) -> Response[SchemaType]
+
update(
+    session: AsyncSession,
+    obj: BaseModel,
+    filters: list[Any],
+    *,
+    exclude_unset: bool = True,
+    exclude_none: bool = False,
+    with_for_update: _ForUpdateMode = False,
+    schema: None = ...,
+) -> ModelType
+
+ + +
+ +

Update a record in the database.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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 +
+ with_for_update + + _ForUpdateMode + +
+

Lock the row before updating. True for plain +FOR UPDATE, "nowait" for FOR UPDATE NOWAIT, +"skip_locked" for FOR UPDATE SKIP LOCKED.

+
+
+ 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:

+ + + + + + + + + + + + + +
TypeDescription
+ ModelType | Response[Any] + +
+

Updated model instance, or Response[schema] when schema is given.

+
+
+ + +

Raises:

+ + + + + + + + + + + + + +
TypeDescription
+ NotFoundError + +
+

If no record found

+
+
+ + +
+ +
+ +
+ + +

+ upsert(session, obj, index_elements, *, set_=None, where=None) + + + async + classmethod + + +

+ + +
+ +

Create or update a record (PostgreSQL only).

+

Uses INSERT ... ON CONFLICT for atomic upsert.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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:

+ + + + + + + + + + + + + +
TypeDescription
+ ModelType | None + +
+

Model instance

+
+
+ + +
+ +
+ + + +
+ +
+ +
+ +
+ + +

+ fastapi_toolsets.crud.factory.CrudFactory(model, *, base_class=AsyncCrud, searchable_fields=None, facet_fields=None, order_fields=None, m2m_fields=None, default_load_options=None, cursor_column=None) + +

+ + +
+ +

Create a CRUD class for a specific model.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ model + + type[ModelType] + +
+

SQLAlchemy model class

+
+
+ required +
+ base_class + + type[AsyncCrud[Any]] + +
+

Optional base class to inherit from instead of AsyncCrud. +Use this to share custom methods across multiple CRUD classes while +still using the factory shorthand.

+
+
+ AsyncCrud +
+ 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[OrderFieldType] | None + +
+

Optional list of model attributes that callers are allowed to order by +via offset_paginate_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:

+ + + + + + + + + + + + + +
TypeDescription
+ 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,
+)
+
+# With a shared custom base class:
+from typing import Generic, TypeVar
+from sqlalchemy.orm import DeclarativeBase
+
+T = TypeVar("T", bound=DeclarativeBase)
+
+class AuditedCrud(AsyncCrud[T], Generic[T]):
+    @classmethod
+    async def get_active(cls, session):
+        return await cls.get_multi(session, filters=[cls.model.is_active == True])
+
+UserCrud = CrudFactory(User, base_class=AuditedCrud)
+
+
+ +
+ +
+ +
+ + + +

+ fastapi_toolsets.crud.search.SearchConfig + + + + dataclass + + +

+ + +
+ + + +

Advanced search configuration.

+ + +

Attributes:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
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

+
+
+ + + + + + + + + + + +
+ + + + + + + + + + + + +
+ +
+ +
+ +
+ + +

+ fastapi_toolsets.crud.search.get_searchable_fields(model, *, include_relationships=True, max_depth=1) + + + cached + + +

+ + +
+ +

Auto-detect String fields on a model and its relationships.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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:

+ + + + + + + + + + + + + +
TypeDescription
+ list[SearchFieldType] + +
+

List of columns and tuples (relationship, column)

+
+
+ + +
+ +
+ +
+ + +

+ fastapi_toolsets.crud.search.build_search_filters(model, search, search_fields=None, default_fields=None, search_column=None) + +

+ + +
+ +

Build SQLAlchemy filter conditions for search.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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 +
+ search_column + + str | None + +
+

Optional key to narrow search to a single field. +Must match one of the resolved search field keys.

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ tuple[list[ColumnElement[bool]], list[InstrumentedAttribute[Any]]] + +
+

Tuple of (filter_conditions, joins_needed)

+
+
+ + +

Raises:

+ + + + + + + + + + + + + +
TypeDescription
+ NoSearchableFieldsError + +
+

If no searchable field has been configured

+
+
+ + +
+ +
+ + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/v5.1/reference/db/index.html b/v5.1/reference/db/index.html new file mode 100644 index 0000000..3a5a5a7 --- /dev/null +++ b/v5.1/reference/db/index.html @@ -0,0 +1,4206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + db - FastAPI Toolsets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + +

db

+

Here's the reference for the Database facade, the transaction helper, locking +functions, many-to-many helpers, and row-watching utilities.

+

You can import them directly from fastapi_toolsets.db:

+
from fastapi_toolsets.db import (
+    Database,
+    LockMode,
+    advisory_lock,
+    lock_tables,
+    m2m_add,
+    m2m_remove,
+    m2m_set,
+    transaction,
+    wait_for_row_change,
+)
+
+ + +
+ + + +

+ fastapi_toolsets.db.Database + + +

+ + +
+ + + +

One object that owns the engine, sessions, dependency, and middleware.

+

Provide exactly one of url (the facade builds and disposes the engine) or +engine (an engine you own, e.g. for Alembic or event.listen, left +untouched).

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ url + + str | PostgresDsn | None + +
+

Database connection URL. Accepts a plain string or a Pydantic +:class:~pydantic.PostgresDsn.

+
+
+ None +
+ engine + + AsyncEngine | None + +
+

An existing :class:AsyncEngine to reuse instead of url.

+
+
+ None +
+ session_class + + type[AsyncSession] + +
+

Session class for the sessionmaker (e.g. EventSession).

+
+
+ AsyncSession +
+ expire_on_commit + + bool + +
+

Expire attributes after commit. Defaults to False.

+
+
+ False +
+ autoflush + + bool + +
+

Autoflush the session before queries. Defaults to True.

+
+
+ True +
+ connect_args + + dict[str, Any] | None + +
+

DBAPI-level connection arguments forwarded to +:func:create_async_engine (URL mode only).

+
+
+ None +
+ **engine_options + + Any + +
+

Extra keyword arguments forwarded to +:func:create_async_engine (URL mode only).

+
+
+ {} +
+ + +

Raises:

+ + + + + + + + + + + + + +
TypeDescription
+ TypeError + +
+

If neither or both of url and engine are given, or if +connect_args/engine_options are passed together with engine.

+
+
+ + +
+ Example +
from fastapi import Depends, FastAPI
+from fastapi_toolsets.db import Database
+
+db = Database("postgresql+asyncpg://postgres:postgres@localhost/app")
+
+app = FastAPI()
+db.install(app)
+
+@app.get("/users/{user_id}")
+async def get_user(user_id: int, session=Depends(db)):
+    return await UserCrud.get(session, [User.id == user_id])
+
+
+ + + + + + + + + + +
+ + + + + + + + + + +
+ + +

+ __call__(request) + + + async + + +

+ + +
+ +

FastAPI dependency: yield a session and commit once at the right time.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ request + + Request + +
+

The incoming request (injected by FastAPI).

+
+
+ required +
+ + +

Yields:

+ + + + + + + + + + + + + +
TypeDescription
+ AsyncGenerator[AsyncSession, None] + +
+

An AsyncSession for the duration of the request.

+
+
+ + +
+ Example +
@app.get("/users/{user_id}")
+async def get_user(user_id: int, session=Depends(db)):
+    return await UserCrud.get(session, [User.id == user_id])
+
+
+ +
+ +
+ +
+ + +

+ begin() + + + async + + +

+ + +
+ +

Open a session already inside a transaction (sugar for the common case).

+

Equivalent to session() + :func:transaction. Commits on clean exit, +rolls back on exception.

+ + +

Yields:

+ + + + + + + + + + + + + +
TypeDescription
+ AsyncGenerator[AsyncSession, None] + +
+

An AsyncSession open within a transaction.

+
+
+ + +
+ Example +
async with db.begin() as session:
+    session.add(User(name="ada"))
+
+
+ +
+ +
+ +
+ + +

+ install(app) + +

+ + +
+ +

Wire the commit middleware and engine disposal onto app.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ app + + Any + +
+

The FastAPI/Starlette application to wire.

+
+
+ required +
+ + +
+ Example +
@asynccontextmanager
+async def lifespan(app):
+    ...  # your startup
+    yield
+    ...  # your shutdown
+
+app = FastAPI(lifespan=lifespan)
+db.install(app)
+
+
+ +
+ +
+ +
+ + +

+ lifespan(app) + + + async + + +

+ + +
+ +

Dispose the engine on shutdown; use as FastAPI(lifespan=db.lifespan).

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ app + + Any + +
+

The ASGI application (unused; required by the lifespan protocol).

+
+
+ required +
+ + +

Yields:

+ + + + + + + + + + + + + +
TypeDescription
+ AsyncGenerator[None, None] + +
+

Control to the application for its lifetime.

+
+
+ + +
+ Example +
app = FastAPI(lifespan=db.lifespan)
+
+
+ +
+ +
+ +
+ + +

+ lock_tables(tables, *, mode=LockMode.SHARE_UPDATE_EXCLUSIVE, timeout='5s') + +

+ + +
+ +

Lock PostgreSQL tables for the duration of a dedicated transaction.

+

Opens its own session from the facade's sessionmaker, changes are +committed when the context exits.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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:

+ + + + + + + + + + + + + +
TypeDescription
+ AbstractAsyncContextManager[AsyncSession] + +
+

The dedicated session, open within the locked transaction.

+
+
+ + +

Raises:

+ + + + + + + + + + + + + + + + + +
TypeDescription
+ LockTimeoutError + +
+

If the lock cannot be acquired within timeout.

+
+
+ PoolExhaustedError + +
+

If the connection pool is exhausted.

+
+
+ + +
+ Example +
async with db.lock_tables([User, Account]) as session:
+    user = await UserCrud.get(session, [User.id == 1])
+    user.balance += 100
+
+
+ +
+ +
+ +
+ + +

+ session() + + + async + + +

+ + +
+ +

Open a session outside request handlers (background tasks, CLI, tests).

+

Commits on clean exit, rolls back on exception.

+ + +

Yields:

+ + + + + + + + + + + + + +
TypeDescription
+ AsyncGenerator[AsyncSession, None] + +
+

An AsyncSession ready for database operations.

+
+
+ + +
+ Example +
async with db.session() as session:
+    user = await UserCrud.get(session, [User.id == 1])
+
+
+ +
+ +
+ + + +
+ +
+ +
+ +
+ + +

+ fastapi_toolsets.db.transaction(session) + + + async + + +

+ + +
+ +

Run a block inside a savepoint-aware transaction.

+

If session is already in a transaction, a nested transaction (savepoint) +is opened so the block can roll back independently. Otherwise a top-level +transaction is started. Commits on clean exit, rolls back on exception.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ session + + AsyncSession + +
+

AsyncSession instance.

+
+
+ required +
+ + +

Yields:

+ + + + + + + + + + + + + +
TypeDescription
+ AsyncGenerator[AsyncSession, None] + +
+

The session within the transaction context.

+
+
+ + +
+ Example +
from fastapi_toolsets.db import transaction
+
+async with transaction(session):
+    session.add(model)
+
+
+ +
+ +
+ +
+ + + +

+ fastapi_toolsets.db.LockMode + + +

+ + +
+

+ Bases: str, Enum

+ + + +

PostgreSQL table lock modes.

+

See: https://www.postgresql.org/docs/current/explicit-locking.html

+ + + + + + + + + + + +
+ + + + + + + + + + + + +
+ +
+ +
+ +
+ + +

+ fastapi_toolsets.db.lock_tables(session_maker, tables, *, mode=LockMode.SHARE_UPDATE_EXCLUSIVE, timeout='5s') + +

+ + +
+ +

Lock PostgreSQL tables for the duration of a transaction.

+

Prefer the method on a :class:Database instance; use this +directly only when you manage your own session factory.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ session_maker + + async_sessionmaker[_SessionT] + +
+

Async session factory used to create the dedicated +session.

+
+
+ 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:

+ + + + + + + + + + + + + +
TypeDescription
+ AbstractAsyncContextManager[_SessionT] + +
+

The dedicated session, open within the locked transaction.

+
+
+ + +

Raises:

+ + + + + + + + + + + + + + + + + +
TypeDescription
+ LockTimeoutError + +
+

If the lock cannot be acquired within timeout.

+
+
+ PoolExhaustedError + +
+

If the connection pool is exhausted.

+
+
+ + +
+ Example +
from fastapi_toolsets.db import lock_tables
+
+async with lock_tables(session_maker, [User, Account]) as session:
+    user = await UserCrud.get(session, [User.id == 1])
+    user.balance += 100
+
+
+ +
+ +
+ +
+ + +

+ fastapi_toolsets.db.advisory_lock(session, key, *, shared=False, nowait=False, timeout=None) + + + async + + +

+ + +
+ +

Acquire a PostgreSQL session-level advisory lock.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ session + + AsyncSession + +
+

AsyncSession instance.

+
+
+ required +
+ key + + int | tuple[int, int] + +
+

Lock key, either a single int (bigint) or a (int, int) pair for namespacing.

+
+
+ required +
+ shared + + bool + +
+

Acquire a shared lock (multiple holders allowed). Default is exclusive.

+
+
+ False +
+ nowait + + bool + +
+

Return False immediately if the lock is unavailable instead of waiting.

+
+
+ False +
+ timeout + + str | None + +
+

Maximum wait time (e.g. "5s", "500ms"). Raises DBAPIError +if exceeded. Ignored when nowait is True.

+
+
+ None +
+ + +

Yields:

+ + + + + + + + + + + + + + + + + +
TypeDescription
+ AsyncGenerator[bool, None] + +
+

True if the lock was acquired, False if nowait is True and the lock

+
+
+ AsyncGenerator[bool, None] + +
+

is already held.

+
+
+ + +

Raises:

+ + + + + + + + + + + + + +
TypeDescription
+ LockTimeoutError + +
+

If timeout is set and the lock cannot be acquired in time.

+
+
+ + +
+ Example +
from fastapi_toolsets.db import advisory_lock
+
+async with advisory_lock(session, 42):
+    ...
+
+async with advisory_lock(session, 42, nowait=True) as acquired:
+    if not acquired:
+        raise HTTPException(409, "Resource is locked")
+
+async with advisory_lock(session, 42, timeout="5s"):
+    ...
+
+async with advisory_lock(session, (1, user_id), shared=True):
+    ...
+
+
+ +
+ +
+ +
+ + +

+ fastapi_toolsets.db.m2m_add(session, instance, rel_attr, *related, ignore_conflicts=False) + + + async + + +

+ + +
+ +

Insert rows into a Many-to-Many association table without loading the ORM collection.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ session + + AsyncSession + +
+

DB async session.

+
+
+ required +
+ instance + + DeclarativeBase + +
+

The "owner" side model instance (e.g. the A in A.b_list).

+
+
+ required +
+ rel_attr + + QueryableAttribute + +
+

The M2M relationship attribute on the model class (e.g. A.b_list).

+
+
+ required +
+ *related + + DeclarativeBase + +
+

One or more related instances to associate with instance.

+
+
+ () +
+ ignore_conflicts + + bool + +
+

When True, silently skip rows that already exist +in the association table (ON CONFLICT DO NOTHING).

+
+
+ False +
+ + +

Raises:

+ + + + + + + + + + + + + +
TypeDescription
+ TypeError + +
+

If rel_attr is not a Many-to-Many relationship.

+
+
+ + +
+ Example +
from fastapi_toolsets.db import m2m_add, transaction
+
+async with transaction(session):
+    await m2m_add(session, post, Post.tags, tag1, tag2)
+
+
+ +
+ +
+ +
+ + +

+ fastapi_toolsets.db.m2m_remove(session, instance, rel_attr, *related) + + + async + + +

+ + +
+ +

Remove rows from a Many-to-Many association table without loading the ORM collection.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ session + + AsyncSession + +
+

DB async session.

+
+
+ required +
+ instance + + DeclarativeBase + +
+

The "owner" side model instance (e.g. the A in A.b_list).

+
+
+ required +
+ rel_attr + + QueryableAttribute + +
+

The M2M relationship attribute on the model class (e.g. A.b_list).

+
+
+ required +
+ *related + + DeclarativeBase + +
+

One or more related instances to disassociate from instance.

+
+
+ () +
+ + +

Raises:

+ + + + + + + + + + + + + +
TypeDescription
+ TypeError + +
+

If rel_attr is not a Many-to-Many relationship.

+
+
+ + +
+ Example +
from fastapi_toolsets.db import m2m_remove, transaction
+
+async with transaction(session):
+    await m2m_remove(session, post, Post.tags, tag1)
+
+
+ +
+ +
+ +
+ + +

+ fastapi_toolsets.db.m2m_set(session, instance, rel_attr, *related) + + + async + + +

+ + +
+ +

Replace the entire Many-to-Many association set atomically.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ session + + AsyncSession + +
+

DB async session.

+
+
+ required +
+ instance + + DeclarativeBase + +
+

The "owner" side model instance (e.g. the A in A.b_list).

+
+
+ required +
+ rel_attr + + QueryableAttribute + +
+

The M2M relationship attribute on the model class (e.g. A.b_list).

+
+
+ required +
+ *related + + DeclarativeBase + +
+

The new complete set of related instances.

+
+
+ () +
+ + +

Raises:

+ + + + + + + + + + + + + +
TypeDescription
+ TypeError + +
+

If rel_attr is not a Many-to-Many relationship.

+
+
+ + +
+ Example +
from fastapi_toolsets.db import m2m_set, transaction
+
+async with transaction(session):
+    await m2m_set(session, post, Post.tags, tag1, tag2)  # replaces all
+
+
+ +
+ +
+ +
+ + +

+ fastapi_toolsets.db.wait_for_row_change(session, model, pk_value, *, columns=None, interval=0.5, timeout=None) + + + async + + +

+ + +
+ +

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:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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:

+ + + + + + + + + + + + + +
TypeDescription
+ _M + +
+

The refreshed model instance with updated values.

+
+
+ + +

Raises:

+ + + + + + + + + + + + + + + + + +
TypeDescription
+ 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,
+)
+
+
+ +
+ +

Admin and test helpers live in fastapi_toolsets.db.testing:

+
from fastapi_toolsets.db.testing import cleanup_tables, create_database
+
+ + +
+ + +

+ fastapi_toolsets.db.testing.create_database(db_name, *, server_url) + + + async + + +

+ + +
+ +

Create a database.

+

Connects to server_url using AUTOCOMMIT isolation and issues a +CREATE DATABASE statement for db_name.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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.testing import create_database
+
+SERVER_URL = "postgresql+asyncpg://postgres:postgres@localhost/postgres"
+await create_database("myapp_test", server_url=SERVER_URL)
+
+
+ +
+ +
+ +
+ + +

+ fastapi_toolsets.db.testing.cleanup_tables(session, base) + + + async + + +

+ + +
+ +

Truncate all tables for fast between-test cleanup.

+

Executes a single TRUNCATE … RESTART IDENTITY CASCADE statement +across every table in base's metadata.

+

This is a no-op when the metadata contains no tables.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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)
+
+
+ +
+ +
+ + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/v5.1/reference/dependencies/index.html b/v5.1/reference/dependencies/index.html new file mode 100644 index 0000000..bd78d0c --- /dev/null +++ b/v5.1/reference/dependencies/index.html @@ -0,0 +1,2175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dependencies - FastAPI Toolsets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+ + + + + + + + +
+ +
+ + +
+ + + + +
+
+
+ + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + +

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

+ fastapi_toolsets.dependencies.PathDependency(model, field, *, session_dep, param_name=None) + +

+ + +
+ +

Create a dependency that fetches a DB object from a path parameter.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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:

+ + + + + + + + + + + + + +
TypeDescription
+ ModelType + +
+

A Depends() instance that resolves to the model instance

+
+
+ + +

Raises:

+ + + + + + + + + + + + + +
TypeDescription
+ 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:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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:

+ + + + + + + + + + + + + +
TypeDescription
+ ModelType + +
+

A Depends() instance that resolves to the model instance

+
+
+ + +

Raises:

+ + + + + + + + + + + + + +
TypeDescription
+ 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,
+): ...
+
+
+ +
+ +
+ + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/v5.1/reference/exceptions/index.html b/v5.1/reference/exceptions/index.html new file mode 100644 index 0000000..480068d --- /dev/null +++ b/v5.1/reference/exceptions/index.html @@ -0,0 +1,3992 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + exceptions - FastAPI Toolsets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + +

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,
+    InvalidSearchColumnError,
+    InvalidFacetFilterError,
+    InvalidOrderFieldError,
+    PoolExhaustedError,
+    LockTimeoutError,
+    generate_error_responses,
+    init_exceptions_handlers,
+)
+
+ + +
+ + + +

+ fastapi_toolsets.exceptions.exceptions.ApiException + + +

+ + +
+

+ Bases: Exception

+ + + +

Base exception for API errors with structured response.

+ + + + + + + + + + + +
+ + + + + + + + + + +
+ + +

+ __init__(detail=None, *, desc=None, data=None) + +

+ + +
+ +

Initialize the exception.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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 +
+ + +
+ +
+ + + +
+ +
+ +
+ +
+ + + +

+ fastapi_toolsets.exceptions.exceptions.UnauthorizedError + + +

+ + +
+

+ Bases: ApiException

+ + + +

HTTP 401 - User is not authenticated.

+ + + + + + + + + + + +
+ + + + + + + + + + +
+ + +

+ __init__(detail=None, *, desc=None, data=None) + +

+ + +
+ +

Initialize the exception.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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 +
+ + +
+ +
+ + + +
+ +
+ +
+ +
+ + + +

+ fastapi_toolsets.exceptions.exceptions.ForbiddenError + + +

+ + +
+

+ Bases: ApiException

+ + + +

HTTP 403 - User lacks required permissions.

+ + + + + + + + + + + +
+ + + + + + + + + + +
+ + +

+ __init__(detail=None, *, desc=None, data=None) + +

+ + +
+ +

Initialize the exception.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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 +
+ + +
+ +
+ + + +
+ +
+ +
+ +
+ + + +

+ fastapi_toolsets.exceptions.exceptions.NotFoundError + + +

+ + +
+

+ Bases: ApiException

+ + + +

HTTP 404 - Resource not found.

+ + + + + + + + + + + +
+ + + + + + + + + + +
+ + +

+ __init__(detail=None, *, desc=None, data=None) + +

+ + +
+ +

Initialize the exception.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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 +
+ + +
+ +
+ + + +
+ +
+ +
+ +
+ + + +

+ fastapi_toolsets.exceptions.exceptions.ConflictError + + +

+ + +
+

+ Bases: ApiException

+ + + +

HTTP 409 - Resource conflict.

+ + + + + + + + + + + +
+ + + + + + + + + + +
+ + +

+ __init__(detail=None, *, desc=None, data=None) + +

+ + +
+ +

Initialize the exception.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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 +
+ + +
+ +
+ + + +
+ +
+ +
+ +
+ + + +

+ fastapi_toolsets.exceptions.exceptions.NoSearchableFieldsError + + +

+ + +
+

+ Bases: ApiException

+ + + +

Raised when search is requested but no searchable fields are available.

+ + + + + + + + + + + +
+ + + + + + + + + + +
+ + +

+ __init__(model) + +

+ + +
+ +

Initialize the exception.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ model + + type + +
+

The model class that has no searchable fields configured.

+
+
+ required +
+ + +
+ +
+ + + +
+ +
+ +
+ +
+ + + +

+ fastapi_toolsets.exceptions.exceptions.InvalidSearchColumnError + + +

+ + +
+

+ Bases: ApiException

+ + + +

Raised when search_column is not one of the configured searchable fields.

+ + + + + + + + + + + +
+ + + + + + + + + + +
+ + +

+ __init__(column, valid_columns) + +

+ + +
+ +

Initialize the exception.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ column + + str + +
+

The unknown search column provided by the caller.

+
+
+ required +
+ valid_columns + + list[str] + +
+

List of valid search column keys.

+
+
+ required +
+ + +
+ +
+ + + +
+ +
+ +
+ +
+ + + +

+ fastapi_toolsets.exceptions.exceptions.InvalidFacetFilterError + + +

+ + +
+

+ Bases: ApiException

+ + + +

Raised when filter_by contains a key not declared in facet_fields.

+ + + + + + + + + + + +
+ + + + + + + + + + +
+ + +

+ __init__(key, valid_keys) + +

+ + +
+ +

Initialize the exception.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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 +
+ + +
+ +
+ + + +
+ +
+ +
+ +
+ + + +

+ fastapi_toolsets.exceptions.exceptions.InvalidOrderFieldError + + +

+ + +
+

+ Bases: ApiException

+ + + +

Raised when order_by contains a field not in the allowed order fields.

+ + + + + + + + + + + +
+ + + + + + + + + + +
+ + +

+ __init__(field, valid_fields) + +

+ + +
+ +

Initialize the exception.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ field + + str + +
+

The unknown order field provided by the caller.

+
+
+ required +
+ valid_fields + + list[str] + +
+

List of valid field names.

+
+
+ required +
+ + +
+ +
+ + + +
+ +
+ +
+ +
+ + + +

+ fastapi_toolsets.exceptions.exceptions.PoolExhaustedError + + +

+ + +
+

+ Bases: ApiException

+ + + +

HTTP 503 - Database connection pool is exhausted.

+ + + + + + + + + + + +
+ + + + + + + + + + +
+ + +

+ __init__(detail=None, *, desc=None, data=None) + +

+ + +
+ +

Initialize the exception.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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 +
+ + +
+ +
+ + + +
+ +
+ +
+ +
+ + + +

+ fastapi_toolsets.exceptions.exceptions.LockTimeoutError + + +

+ + +
+

+ Bases: ApiException

+ + + +

HTTP 503 - A database lock could not be acquired within the timeout.

+ + + + + + + + + + + +
+ + + + + + + + + + +
+ + +

+ __init__(detail=None, *, desc=None, data=None) + +

+ + +
+ +

Initialize the exception.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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 +
+ + +
+ +
+ + + +
+ +
+ +
+ +
+ + +

+ fastapi_toolsets.exceptions.exceptions.generate_error_responses(*errors) + +

+ + +
+ +

Generate OpenAPI response documentation for exceptions.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ *errors + + type[ApiException] + +
+

Exception classes that inherit from ApiException.

+
+
+ () +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ dict[int | str, dict[str, Any]] + +
+

Dict suitable for FastAPI's responses parameter.

+
+
+ + +
+ +
+ +
+ + +

+ fastapi_toolsets.exceptions.handler.init_exceptions_handlers(app) + +

+ + +
+ +

Register exception handlers and custom OpenAPI schema on a FastAPI app.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ app + + FastAPI + +
+

FastAPI application instance.

+
+
+ required +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ FastAPI + +
+

The same FastAPI instance (for chaining).

+
+
+ + +
+ +
+ + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/v5.1/reference/fixtures/index.html b/v5.1/reference/fixtures/index.html new file mode 100644 index 0000000..9903dba --- /dev/null +++ b/v5.1/reference/fixtures/index.html @@ -0,0 +1,4036 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fixtures - FastAPI Toolsets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + +

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,
+)
+
+ + +
+ + + +

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

+ + +
+ +

Test fixtures.

+ +
+ +
+ + + + + + +
+ +
+ +
+ +
+ + + +

+ fastapi_toolsets.fixtures.enum.LoadStrategy + + +

+ + +
+

+ Bases: str, Enum

+ + + +

Strategy for loading fixtures into the database.

+ + + + + + + + + + + +
+ + + + + + + +
+ + + +

+ INSERT = 'insert' + + + class-attribute + instance-attribute + + +

+ + +
+ +

Insert new records. Fails if record already exists.

+ +
+ +
+ +
+ + + +

+ MERGE = 'merge' + + + class-attribute + instance-attribute + + +

+ + +
+ +

Insert or update based on primary key (SQLAlchemy merge).

+ +
+ +
+ +
+ + + +

+ SKIP_EXISTING = 'skip_existing' + + + class-attribute + instance-attribute + + +

+ + +
+ +

Insert only if record doesn't exist (based on primary key).

+ +
+ +
+ + + + + + +
+ +
+ +
+ +
+ + + +

+ fastapi_toolsets.fixtures.registry.Fixture + + + + dataclass + + +

+ + +
+ + + +

A fixture definition with metadata.

+ + + + + + + + + + + +
+ + + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ fastapi_toolsets.fixtures.registry.FixtureRegistry + + +

+ + +
+ + + +

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),
+    ]
+
+

Fixtures with the same name may be registered for different contexts. +When multiple contexts are loaded together, their instances are merged:

+
```python
+@fixtures.register(contexts=[Context.BASE])
+def users():
+    return [User(id=1, username="admin")]
+
+@fixtures.register(contexts=[Context.TESTING])
+def users():
+    return [User(id=2, username="tester")]
+```
+
+ + + + + + + + + + + +
+ + + + + + + + + + +
+ + +

+ field(name, attr_name, value, *, field='id') + +

+ + +
+ +

Get a single field value from a fixture object matched by an attribute.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ name + + str + +
+

Fixture name to look up.

+
+
+ required +
+ attr_name + + str + +
+

Name of the attribute to match against.

+
+
+ required +
+ value + + Any + +
+

Value to match.

+
+
+ required +
+ field + + str + +
+

Attribute name to return from the matched object (default: "id").

+
+
+ 'id' +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ Any + +
+

The value of field on the first matching model instance.

+
+
+ + +

Raises:

+ + + + + + + + + + + + + + + + + +
TypeDescription
+ KeyError + +
+

If no fixture named name is registered.

+
+
+ StopIteration + +
+

If no matching object is found.

+
+
+ + +
+ +
+ +
+ + +

+ get(name) + +

+ + +
+ +

Get a fixture by name.

+ + +

Raises:

+ + + + + + + + + + + + + + + + + +
TypeDescription
+ KeyError + +
+

If no fixture with name is registered.

+
+
+ ValueError + +
+

If the fixture has multiple context variants — use +:meth:get_variants in that case.

+
+
+ + +
+ +
+ +
+ + +

+ get_all() + +

+ + +
+ +

Get all registered fixtures (all variants of all names).

+ + +
+ +
+ +
+ + +

+ get_by_context(*contexts) + +

+ + +
+ +

Get fixtures for specific contexts.

+ + +
+ +
+ +
+ + +

+ get_dependencies(name) + +

+ + +
+ +

Get the union of depends_on across all variants of name.

+ + +

Raises:

+ + + + + + + + + + + + + +
TypeDescription
+ KeyError + +
+

If no fixture named name is registered.

+
+
+ + +
+ +
+ +
+ + +

+ get_load_variants(name, *contexts) + +

+ + +
+ +

Return variants for name filtered by contexts.

+ + +

Raises:

+ + + + + + + + + + + + + +
TypeDescription
+ KeyError + +
+

If no fixture with name is registered.

+
+
+ + +
+ +
+ +
+ + +

+ get_variants(name, *contexts) + +

+ + +
+ +

Return all registered variants for name, optionally filtered by context.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ name + + str + +
+

Fixture name.

+
+
+ required +
+ *contexts + + str | Enum + +
+

If given, only return variants whose context set +intersects with these values (:class:Context.BASE variants +are always included). Both :class:Context enum values and +plain strings are accepted.

+
+
+ () +
+ + +

Returns:

+ + + + + + + + + + + + + + + + + +
TypeDescription
+ list[Fixture] + +
+

List of matching :class:Fixture objects (may be empty when a

+
+
+ list[Fixture] + +
+

context filter is applied and nothing matches).

+
+
+ + +

Raises:

+ + + + + + + + + + + + + +
TypeDescription
+ KeyError + +
+

If no fixture with name is registered.

+
+
+ + +
+ +
+ +
+ + +

+ include_registry(registry) + +

+ + +
+ +

Include another FixtureRegistry in the same current FixtureRegistry.

+

Fixtures with the same name are allowed as long as their context sets +do not overlap. Conflicting contexts raise :class:ValueError.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ registry + + FixtureRegistry + +
+

The FixtureRegistry to include

+
+
+ required +
+ + +

Raises:

+ + + + + + + + + + + + + +
TypeDescription
+ ValueError + +
+

If a fixture name already exists with overlapping contexts

+
+
+ + +
+ Example +
registry = FixtureRegistry()
+dev_registry = FixtureRegistry()
+
+@dev_registry.register
+def dev_data():
+    return [...]
+
+registry.include_registry(registry=dev_registry)
+
+
+ +
+ +
+ +
+ + +

+ obj(name, attr_name, value) + +

+ + +
+ +

Get a model instance from a registered fixture by attribute value.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ name + + str + +
+

Fixture name to look up.

+
+
+ required +
+ attr_name + + str + +
+

Name of the attribute to match against.

+
+
+ required +
+ value + + Any + +
+

Value to match.

+
+
+ required +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ DeclarativeBase + +
+

The first model instance where the attribute matches the given value.

+
+
+ + +

Raises:

+ + + + + + + + + + + + + + + + + +
TypeDescription
+ KeyError + +
+

If no fixture named name is registered.

+
+
+ StopIteration + +
+

If no matching object is found.

+
+
+ + +
+ +
+ +
+ + +

+ register(func=None, *, name=None, depends_on=None, contexts=None) + +

+ + +
+ +

Register a fixture function.

+

Can be used as a decorator with or without arguments.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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 | Enum] | None + +
+

List of contexts this fixture belongs to. Both +:class:Context enum values and plain strings are accepted.

+
+
+ None +
+ + +
+ Example +

```python +@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:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ *contexts + + str | Enum + +
+

Contexts to load

+
+
+ () +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ list[str] + +
+

List of fixture names in load order

+
+
+ + +
+ +
+ +
+ + +

+ resolve_dependencies(*names) + +

+ + +
+ +

Resolve fixture dependencies in topological order.

+

When a fixture name has multiple context variants, the union of all +variants' depends_on lists is used.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ *names + + str + +
+

Fixture names to resolve

+
+
+ () +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ list[str] + +
+

List of fixture names in load order (dependencies first)

+
+
+ + +

Raises:

+ + + + + + + + + + + + + + + + + +
TypeDescription
+ KeyError + +
+

If a fixture is not found

+
+
+ ValueError + +
+

If circular dependency detected

+
+
+ + +
+ +
+ + + +
+ +
+ +
+ +
+ + +

+ fastapi_toolsets.fixtures.utils.load_fixtures(session, registry, *names, strategy=LoadStrategy.MERGE) + + + async + + +

+ + +
+ +

Load specific fixtures by name with dependencies.

+

All context variants of each requested fixture are loaded and merged.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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:

+ + + + + + + + + + + + + +
TypeDescription
+ 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:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ session + + AsyncSession + +
+

Database session

+
+
+ required +
+ registry + + FixtureRegistry + +
+

Fixture registry

+
+
+ required +
+ *contexts + + str | Enum + +
+

Contexts to load (e.g., Context.TESTING, or plain +strings for custom contexts)

+
+
+ () +
+ strategy + + LoadStrategy + +
+

How to handle existing records

+
+
+ MERGE +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ dict[str, list[DeclarativeBase]] + +
+

Dict mapping fixture names to loaded instances

+
+
+ + +
+ +
+ + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/v5.1/reference/logger/index.html b/v5.1/reference/logger/index.html new file mode 100644 index 0000000..db9e4f3 --- /dev/null +++ b/v5.1/reference/logger/index.html @@ -0,0 +1,2074 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + logger - FastAPI Toolsets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+ + + + + + + + +
+ +
+ + +
+ + + + +
+
+
+ + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + +

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

+ fastapi_toolsets.logger.configure_logging(level='INFO', fmt=DEFAULT_FORMAT, logger_name=None) + +

+ + +
+ +

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:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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:

+ + + + + + + + + + + + + +
TypeDescription
+ Logger + +
+

The configured Logger instance.

+
+
+ + +
+ Example +
from fastapi_toolsets.logger import configure_logging
+
+logger = configure_logging("DEBUG")
+logger.info("Application started")
+
+
+ +
+ +
+ +
+ + +

+ fastapi_toolsets.logger.get_logger(name=_SENTINEL) + +

+ + +
+ +

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:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ name + + str | None + +
+

Logger name. Defaults to the caller's __name__. +Pass None to get the root logger.

+
+
+ _SENTINEL +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ Logger + +
+

A Logger instance.

+
+
+ + +
+ 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
+
+
+ +
+ +
+ + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/v5.1/reference/metrics/index.html b/v5.1/reference/metrics/index.html new file mode 100644 index 0000000..7e20f67 --- /dev/null +++ b/v5.1/reference/metrics/index.html @@ -0,0 +1,2524 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + metrics - FastAPI Toolsets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+ + + + + + + + +
+ +
+ + +
+ + + + +
+
+
+ + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + +

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

+ fastapi_toolsets.metrics.registry.Metric + + + + dataclass + + +

+ + +
+ + + +

A metric definition with metadata.

+ + + + + + + + + + + +
+ + + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ fastapi_toolsets.metrics.registry.MetricsRegistry + + +

+ + +
+ + + +

Registry for managing Prometheus metric providers and collectors.

+ + + + + + + + + + + +
+ + + + + + + + + + +
+ + +

+ get(name) + +

+ + +
+ +

Return the metric instance created by a provider.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ name + + str + +
+

The metric name (defaults to the provider function name).

+
+
+ required +
+ + +

Raises:

+ + + + + + + + + + + + + +
TypeDescription
+ KeyError + +
+

If the metric name is unknown or init_metrics has not +been called yet.

+
+
+ + +
+ +
+ +
+ + +

+ get_all() + +

+ + +
+ +

Get all registered metric definitions.

+ + +
+ +
+ +
+ + +

+ get_collectors() + +

+ + +
+ +

Get collectors (called on each scrape).

+ + +
+ +
+ +
+ + +

+ get_providers() + +

+ + +
+ +

Get metric providers (called once at init).

+ + +
+ +
+ +
+ + +

+ include_registry(registry) + +

+ + +
+ +

Include another :class:MetricsRegistry into this one.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ registry + + MetricsRegistry + +
+

The registry to merge in.

+
+
+ required +
+ + +

Raises:

+ + + + + + + + + + + + + +
TypeDescription
+ ValueError + +
+

If a metric name already exists in the current registry.

+
+
+ + +
+ +
+ +
+ + +

+ register(func=None, *, name=None, collect=False) + +

+ + +
+ +

Register a metric provider or collector function.

+

Can be used as a decorator with or without arguments.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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 +
+ + +
+ +
+ + + +
+ +
+ +
+ +
+ + +

+ fastapi_toolsets.metrics.handler.init_metrics(app, registry, *, path='/metrics') + +

+ + +
+ +

Register a Prometheus /metrics endpoint on a FastAPI app.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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:

+ + + + + + + + + + + + + +
TypeDescription
+ 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)
+
+
+ +
+ +
+ + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/v5.1/reference/models/index.html b/v5.1/reference/models/index.html new file mode 100644 index 0000000..8828a36 --- /dev/null +++ b/v5.1/reference/models/index.html @@ -0,0 +1,2400 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + models - FastAPI Toolsets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+ + + + + + + + +
+ +
+ + + +
+
+ + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + +

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 (
+    EventSession,
+    ModelEvent,
+    UUIDMixin,
+    UUIDv7Mixin,
+    CreatedAtMixin,
+    UpdatedAtMixin,
+    TimestampMixin,
+    listens_for,
+)
+
+ + +
+ + + +

+ fastapi_toolsets.models.EventSession + + +

+ + +
+

+ Bases: AsyncSession

+ + + +

AsyncSession subclass that dispatches lifecycle callbacks after commit.

+ + + + + + + + + + + +
+ + + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ fastapi_toolsets.models.ModelEvent + + +

+ + +
+

+ Bases: str, Enum

+ + + +

Event types dispatched by :class:EventSession.

+ + + + + + + + + + + +
+ + + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ fastapi_toolsets.models.UUIDMixin + + +

+ + +
+ + + +

Mixin that adds a UUID primary key auto-generated by the database.

+ + + + + + + + + + + +
+ + + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ fastapi_toolsets.models.UUIDv7Mixin + + +

+ + +
+ + + +

Mixin that adds a UUIDv7 primary key auto-generated by the database.

+ + + + + + + + + + + +
+ + + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ fastapi_toolsets.models.CreatedAtMixin + + +

+ + +
+ + + +

Mixin that adds a created_at timestamp column.

+ + + + + + + + + + + +
+ + + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ fastapi_toolsets.models.UpdatedAtMixin + + +

+ + +
+ + + +

Mixin that adds an updated_at timestamp column.

+ + + + + + + + + + + +
+ + + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ fastapi_toolsets.models.TimestampMixin + + +

+ + +
+

+ Bases: CreatedAtMixin, UpdatedAtMixin

+ + + +

Mixin that combines created_at and updated_at timestamp columns.

+ + + + + + + + + + + +
+ + + + + + + + + + + + +
+ +
+ +
+ +
+ + +

+ fastapi_toolsets.models.listens_for(model_class, event_types=None) + +

+ + +
+ +

Register a callback for one or more model lifecycle events.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ model_class + + type + +
+

The SQLAlchemy model class to listen on.

+
+
+ required +
+ event_types + + list[ModelEvent] | None + +
+

List of :class:ModelEvent values to listen for. +Defaults to all event types.

+
+
+ None +
+ + +
+ +
+ + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/v5.1/reference/pytest/index.html b/v5.1/reference/pytest/index.html new file mode 100644 index 0000000..f0e4568 --- /dev/null +++ b/v5.1/reference/pytest/index.html @@ -0,0 +1,2743 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pytest - FastAPI Toolsets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+ + + + + + + + +
+ +
+ + + +
+
+ + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + +

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,
+)
+
+ + +
+ + +

+ fastapi_toolsets.pytest.plugin.register_fixtures(registry, namespace, *, prefix='fixture_', session_fixture='db_session', strategy=LoadStrategy.MERGE) + +

+ + +
+ +

Register pytest fixtures from a FixtureRegistry.

+

Automatically creates pytest fixtures for each fixture in the registry. +Dependencies are resolved via pytest fixture dependencies.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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:

+ + + + + + + + + + + + + +
TypeDescription
+ 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)
+
+
+ +
+ +
+ +
+ + +

+ fastapi_toolsets.pytest.utils.create_async_client(app, base_url='http://test', dependency_overrides=None, **kwargs) + + + async + + +

+ + +
+ +

Create an async httpx client for testing FastAPI applications.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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 +
+ **kwargs + + Any + +
+

Additional keyword arguments forwarded to +:class:httpx.AsyncClient (e.g. headers, cookies, +auth, timeout).

+
+
+ {} +
+ + +

Yields:

+ + + + + + + + + + + + + +
TypeDescription
+ 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
+
+
+ +
+ +
+ +
+ + +

+ fastapi_toolsets.pytest.utils.create_db_session(database_url, base, *, echo=False, expire_on_commit=False, drop_tables=True, cleanup=False, engine_kwargs=None, session_kwargs=None) + + + async + + +

+ + +
+ +

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:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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 +
+ engine_kwargs + + dict[str, Any] | None + +
+

Additional keyword arguments forwarded to +:func:sqlalchemy.ext.asyncio.create_async_engine +(e.g. pool_size, connect_args).

+
+
+ None +
+ session_kwargs + + dict[str, Any] | None + +
+

Additional keyword arguments forwarded to +:class:sqlalchemy.ext.asyncio.async_sessionmaker +(e.g. autoflush, class_).

+
+
+ None +
+ + +

Yields:

+ + + + + + + + + + + + + +
TypeDescription
+ 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()
+
+
+ +
+ +
+ +
+ + +

+ fastapi_toolsets.pytest.utils.worker_database_url(database_url, default_test_db, *, prefix=None) + +

+ + +
+ +

Derive a per-worker database URL for pytest-xdist parallel runs.

+

Sets the database name to the worker name so each xdist worker operates +on its own database. When not running under xdist, default_test_db is +used instead. When prefix is provided, the name becomes +{prefix}_{worker}.

+

The worker name is read from the PYTEST_XDIST_WORKER environment +variable (set automatically by xdist in each worker process).

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ 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 +
+ prefix + + str | None + +
+

Optional prefix prepended to the worker name +(e.g. "test""test_gw0"). Without it, the database +name is just the worker name (e.g. "gw0").

+
+
+ None +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ str + +
+

A database URL with a worker- or default-specific database name.

+
+
+ + +
+ +
+ +
+ + +

+ fastapi_toolsets.pytest.utils.create_worker_database(database_url, default_test_db='test_db', *, prefix=None, server_url=None) + + + async + + +

+ + +
+ +

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:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ database_url + + str + +
+

Original database connection URL (used 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' +
+ prefix + + str | None + +
+

Optional prefix prepended to the worker name +(e.g. prefix="test""test_gw0"). Without it, the +database name is just the worker name (e.g. "gw0").

+
+
+ None +
+ server_url + + str | None + +
+

URL used for server-level DDL (must point to an existing +database on the same server). Defaults to database_url with the +database omitted, letting asyncpg fall back to the username.

+
+
+ None +
+ + +

Yields:

+ + + + + + + + + + + + + +
TypeDescription
+ 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/myapp"
+
+@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
+
+
+ +
+ +
+ + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/v5.1/reference/schemas/index.html b/v5.1/reference/schemas/index.html new file mode 100644 index 0000000..d2ae106 --- /dev/null +++ b/v5.1/reference/schemas/index.html @@ -0,0 +1,2987 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + schemas - FastAPI Toolsets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + +

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,
+    PaginationType,
+    PaginatedResponse,
+    OffsetPaginatedResponse,
+    CursorPaginatedResponse,
+)
+
+ + +
+ + + +

+ fastapi_toolsets.schemas.PydanticBase + + +

+ + +
+

+ Bases: BaseModel

+ + + +

Base class for all Pydantic models with common configuration.

+ + + + + + + + + + + +
+ + + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ fastapi_toolsets.schemas.ResponseStatus + + +

+ + +
+

+ Bases: str, Enum

+ + + +

Standard API response status.

+ + + + + + + + + + + +
+ + + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ fastapi_toolsets.schemas.ApiError + + +

+ + +
+

+ Bases: PydanticBase

+ + + +

Structured API error definition.

+

Used to define standard error responses with consistent format.

+ + +

Attributes:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
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")

+
+
+ + + + + + + + + + + +
+ + + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ fastapi_toolsets.schemas.BaseResponse + + +

+ + +
+

+ Bases: PydanticBase

+ + + +

Base response structure for all API responses.

+ + +

Attributes:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
status + ResponseStatus + +
+

SUCCESS or FAIL

+
+
message + str + +
+

Human-readable message

+
+
error_code + str | None + +
+

Error code if status is FAIL, None otherwise

+
+
+ + + + + + + + + + + +
+ + + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ fastapi_toolsets.schemas.Response + + +

+ + +
+

+ Bases: BaseResponse, Generic[DataT]

+ + + +

Generic API response with data payload.

+ + +
+ Example +
Response[UserRead](data=user, message="User retrieved")
+
+
+ + + + + + + + + + +
+ + + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ fastapi_toolsets.schemas.ErrorResponse + + +

+ + +
+

+ Bases: BaseResponse

+ + + +

Error response with additional description field.

+

Used for error responses that need more context.

+ + + + + + + + + + + +
+ + + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ fastapi_toolsets.schemas.OffsetPagination + + +

+ + +
+

+ Bases: PydanticBase

+ + + +

Pagination metadata for offset-based list responses.

+ + +

Attributes:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
total_count + int | None + +
+

Total number of items across all pages. +None when include_total=False.

+
+
items_per_page + int + +
+

Number of items per page

+
+
page + int + +
+

Current page number (1-indexed)

+
+
has_more + bool + +
+

Whether there are more pages

+
+
pages + int | None + +
+

Total number of pages

+
+
+ + + + + + + + + + + +
+ + + + + + + +
+ + + +

+ pages + + + property + + +

+ + +
+ +

Total number of pages, or None when total_count is unknown.

+ +
+ +
+ + + + + + +
+ +
+ +
+ +
+ + + +

+ fastapi_toolsets.schemas.CursorPagination + + +

+ + +
+

+ Bases: PydanticBase

+ + + +

Pagination metadata for cursor-based list responses.

+ + +

Attributes:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
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.

+
+
+ + + + + + + + + + + +
+ + + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ fastapi_toolsets.schemas.PaginationType + + +

+ + +
+

+ Bases: str, Enum

+ + + +

Pagination strategy selector for :meth:.AsyncCrud.paginate.

+ + + + + + + + + + + +
+ + + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ fastapi_toolsets.schemas.PaginatedResponse + + +

+ + +
+

+ Bases: BaseResponse, Generic[DataT]

+ + + +

Paginated API response for list endpoints.

+

Base class and return type for endpoints that support both pagination +strategies. Use :class:OffsetPaginatedResponse or +:class:CursorPaginatedResponse when the strategy is fixed.

+

When used as PaginatedResponse[T] in a return annotation, subscripting +returns Annotated[Union[CursorPaginatedResponse[T], OffsetPaginatedResponse[T]], Field(discriminator="pagination_type")] +so FastAPI emits a proper oneOf + discriminator in the OpenAPI schema.

+ + + + + + + + + + + +
+ + + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ fastapi_toolsets.schemas.OffsetPaginatedResponse + + +

+ + +
+

+ Bases: PaginatedResponse[DataT]

+ + + +

Paginated response with typed offset-based pagination metadata.

+

The pagination_type field is always "offset" and acts as a +discriminator, allowing frontend clients to narrow the union type returned +by a unified paginate() endpoint.

+ + + + + + + + + + + +
+ + + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ fastapi_toolsets.schemas.CursorPaginatedResponse + + +

+ + +
+

+ Bases: PaginatedResponse[DataT]

+ + + +

Paginated response with typed cursor-based pagination metadata.

+

The pagination_type field is always "cursor" and acts as a +discriminator, allowing frontend clients to narrow the union type returned +by a unified paginate() endpoint.

+ + + + + + + + + + + +
+ + + + + + + + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/v5.1/search.json b/v5.1/search.json new file mode 100644 index 0000000..2937022 --- /dev/null +++ b/v5.1/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, UUIDv7Mixin, CreatedAtMixin, UpdatedAtMixin, TimestampMixin).
  • Lifecycle Events: Post-commit event system (EventSession, listens_for) that dispatches async/sync callbacks for insert, update, and delete operations.
  • Standardized API Responses: Consistent response format with Response, ErrorResponse, PaginatedResponse, CursorPaginatedResponse and OffsetPaginatedResponse.
  • 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.py
import 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.py
import 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.py
from 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.py
from typing import Annotated\n\nfrom fastapi import Depends\nfrom sqlalchemy.ext.asyncio import AsyncSession\n\nfrom fastapi_toolsets.db import Database\n\nDATABASE_URL = \"postgresql+asyncpg://postgres:postgres@localhost:5432/postgres\"\n\ndb = Database(url=DATABASE_URL)\n\nget_db = db\n\nSessionDep = Annotated[AsyncSession, Depends(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.py
from fastapi import FastAPI\n\nfrom fastapi_toolsets.exceptions import init_exceptions_handlers\n\nfrom .db import db\nfrom .routes import router\n\napp = FastAPI()\ndb.install(app=app)\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":"routes.py:1:16
from typing import Annotated\n\nfrom fastapi import APIRouter, Depends\n\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
","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:19:37
@router.get(\"/offset\")\nasync def list_articles_offset(\n    session: SessionDep,\n    params: Annotated[\n        dict,\n        Depends(\n            ArticleCrud.offset_paginate_params(\n                default_page_size=20,\n                max_page_size=100,\n                default_order_field=Article.created_at,\n            )\n        ),\n    ],\n) -> OffsetPaginatedResponse[ArticleRead]:\n    return await ArticleCrud.offset_paginate(\n        session=session,\n        **params,\n        schema=ArticleRead,\n    )\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  \"pagination_type\": \"offset\",\n  \"data\": [\n    { \"id\": \"3f47ac69-...\", \"title\": \"FastAPI tips\", \"status\": \"published\", ... }\n  ],\n  \"pagination\": {\n    \"total_count\": 42,\n    \"pages\": 5,\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.

To skip the COUNT(*) query for better performance on large tables, pass include_total=False. pagination.total_count will be null in the response, while has_more remains accurate.

","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:40:58
@router.get(\"/cursor\")\nasync def list_articles_cursor(\n    session: SessionDep,\n    params: Annotated[\n        dict,\n        Depends(\n            ArticleCrud.cursor_paginate_params(\n                default_page_size=20,\n                max_page_size=100,\n                default_order_field=Article.created_at,\n            )\n        ),\n    ],\n) -> CursorPaginatedResponse[ArticleRead]:\n    return await ArticleCrud.cursor_paginate(\n        session=session,\n        **params,\n        schema=ArticleRead,\n    )\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  \"pagination_type\": \"cursor\",\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/#unified-endpoint-both-strategies","level":3,"title":"Unified endpoint (both strategies)","text":"

Added in v2.3.0

paginate() lets a single endpoint support both strategies via a pagination_type query parameter. The pagination_type field in the response acts as a discriminator for frontend tooling.

routes.py:61:79
@router.get(\"/\")\nasync def list_articles(\n    session: SessionDep,\n    params: Annotated[\n        dict,\n        Depends(\n            ArticleCrud.paginate_params(\n                default_page_size=20,\n                max_page_size=100,\n                default_order_field=Article.created_at,\n            )\n        ),\n    ],\n) -> PaginatedResponse[ArticleRead]:\n    return await ArticleCrud.paginate(\n        session,\n        **params,\n        schema=ArticleRead,\n    )\n

Offset request (default)

GET /articles/?pagination_type=offset&page=1&items_per_page=10\n
{\n  \"status\": \"SUCCESS\",\n  \"pagination_type\": \"offset\",\n  \"data\": [\"...\"],\n  \"pagination\": { \"total_count\": 42, \"pages\": 5, \"page\": 1, \"items_per_page\": 10, \"has_more\": true }\n}\n

Cursor request

GET /articles/?pagination_type=cursor&items_per_page=10\nGET /articles/?pagination_type=cursor&items_per_page=10&cursor=eyJ2YWx1ZSI6...\n
{\n  \"status\": \"SUCCESS\",\n  \"pagination_type\": \"cursor\",\n  \"data\": [\"...\"],\n  \"pagination\": { \"next_cursor\": \"eyJ2YWx1ZSI6...\", \"prev_cursor\": null, \"items_per_page\": 10, \"has_more\": true }\n}\n
","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():

  1. The as_response parameter is renamed to return_response.
  2. 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(\n    session=session, page=2, items_per_page=20, schema=UserRead\n)\n
result = await UserCrud.offset_paginate(\n    session=session, page=2, items_per_page=20, schema=UserRead\n)\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\n\nclass PaymentRequiredError(BillingError):\n    api_error = ApiError(\n        code=402, msg=\"Payment Required\", desc=\"...\", err_code=\"BILLING-402\"\n    )\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":"migration/v3/","level":1,"title":"Migrating to v3.0","text":"

This page covers every breaking change introduced in v3.0 and the steps required to update your code.

","path":["Migration","Migrating to v3.0"],"tags":[]},{"location":"migration/v3/#crud","level":2,"title":"CRUD","text":"","path":["Migration","Migrating to v3.0"],"tags":[]},{"location":"migration/v3/#facet-keys-now-always-use-the-full-relationship-chain","level":3,"title":"Facet keys now always use the full relationship chain","text":"

In v2, relationship facet fields used only the terminal column key (e.g. \"name\" for Role.name) and only prepended the relationship name when two facet fields shared the same column key. In v3, facet keys always include the full relationship chain joined by __, regardless of collisions.

Before (v2)Now (v3)
User.status -> status\n(User.role, Role.name) -> name\n(User.role, Role.permission, Permission.name) -> name\n
User.status -> status\n(User.role, Role.name) -> role__name\n(User.role, Role.permission, Permission.name) -> role__permission__name\n
","path":["Migration","Migrating to v3.0"],"tags":[]},{"location":"migration/v3/#_params-dependencies-consolidated-into-per-paginate-methods","level":3,"title":"*_params dependencies consolidated into per-paginate methods","text":"

The six individual dependency methods (offset_params, cursor_params, paginate_params, filter_params, search_params, order_params) have been removed and replaced by three consolidated methods that bundle pagination, search, filter, and order into a single Depends() call.

Removed Replacement offset_params() + filter_params() + search_params() + order_params() offset_paginate_params() cursor_params() + filter_params() + search_params() + order_params() cursor_paginate_params() paginate_params() + filter_params() + search_params() + order_params() paginate_params()

Each new method accepts search, filter, and order boolean toggles (all True by default) to disable features you don't need.

Before (v2)Now (v3)
from fastapi_toolsets.crud import OrderByClause\n\n\n@router.get(\"/offset\")\nasync def list_articles_offset(\n    session: SessionDep,\n    params: Annotated[dict, Depends(ArticleCrud.offset_params(default_page_size=20))],\n    filter_by: Annotated[dict, 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        search=search,\n        filter_by=filter_by or None,\n        order_by=order_by,\n        schema=ArticleRead,\n    )\n
@router.get(\"/offset\")\nasync def list_articles_offset(\n    session: SessionDep,\n    params: Annotated[\n        dict,\n        Depends(\n            ArticleCrud.offset_paginate_params(\n                default_page_size=20,\n                default_order_field=Article.created_at,\n            )\n        ),\n    ],\n) -> OffsetPaginatedResponse[ArticleRead]:\n    return await ArticleCrud.offset_paginate(\n        session=session, **params, schema=ArticleRead\n    )\n

The same pattern applies to cursor_paginate_params() and paginate_params(). To disable a feature, pass the toggle:

# No search or ordering, only pagination + filtering\nArticleCrud.offset_paginate_params(search=False, order=False)\n
","path":["Migration","Migrating to v3.0"],"tags":[]},{"location":"migration/v3/#models","level":2,"title":"Models","text":"

The lifecycle event system has been rewritten. Callbacks are now registered with a module-level listens_for decorator and dispatched by EventSession, replacing the mixin-based approach from v2.

","path":["Migration","Migrating to v3.0"],"tags":[]},{"location":"migration/v3/#watchedfieldsmixin-and-watch-removed","level":3,"title":"WatchedFieldsMixin and @watch removed","text":"

Importing WatchedFieldsMixin or watch will raise ImportError.

Model method callbacks (on_create, on_delete, on_update) and the @watch decorator are replaced by:

  1. __watched_fields__ — a plain class attribute to restrict which field changes trigger UPDATE events (replaces @watch).
  2. @listens_for — a module-level decorator to register callbacks for one or more ModelEvent types (replaces on_create / on_delete / on_update methods).
Before (v2)Now (v3)
from fastapi_toolsets.models import WatchedFieldsMixin, watch\n\n\n@watch(\"status\")\nclass Order(Base, UUIDMixin, WatchedFieldsMixin):\n    __tablename__ = \"orders\"\n\n    status: Mapped[str]\n\n    async def on_create(self):\n        await notify_new_order(self.id)\n\n    async def on_update(self, changes):\n        if \"status\" in changes:\n            await notify_status_change(self.id, changes[\"status\"])\n\n    async def on_delete(self):\n        await notify_order_cancelled(self.id)\n
from fastapi_toolsets.models import ModelEvent, UUIDMixin, listens_for\n\n\nclass Order(Base, UUIDMixin):\n    __tablename__ = \"orders\"\n    __watched_fields__ = (\"status\",)\n\n    status: Mapped[str]\n\n\n@listens_for(Order, [ModelEvent.CREATE])\nasync def on_order_created(order: Order, event_type: ModelEvent, changes: None):\n    await notify_new_order(order.id)\n\n\n@listens_for(Order, [ModelEvent.UPDATE])\nasync def on_order_updated(order: Order, event_type: ModelEvent, changes: dict):\n    if \"status\" in changes:\n        await notify_status_change(order.id, changes[\"status\"])\n\n\n@listens_for(Order, [ModelEvent.DELETE])\nasync def on_order_deleted(order: Order, event_type: ModelEvent, changes: None):\n    await notify_order_cancelled(order.id)\n
","path":["Migration","Migrating to v3.0"],"tags":[]},{"location":"migration/v3/#eventsession-now-required","level":3,"title":"EventSession now required","text":"

Without EventSession, lifecycle callbacks will silently stop firing.

Callbacks are now dispatched inside EventSession.commit() rather than via background tasks. Pass it as the session class when creating your session factory:

Before (v2)Now (v3)
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine\n\nengine = create_async_engine(\"postgresql+asyncpg://...\")\nSessionLocal = async_sessionmaker(engine, expire_on_commit=False)\n
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine\nfrom fastapi_toolsets.models import EventSession\n\nengine = create_async_engine(\"postgresql+asyncpg://...\")\nSessionLocal = async_sessionmaker(engine, expire_on_commit=False, class_=EventSession)\n

Note

If you use create_db_session from fastapi_toolsets.pytest, the session already uses EventSession — no changes needed in tests.

","path":["Migration","Migrating to v3.0"],"tags":[]},{"location":"migration/v4/","level":1,"title":"Migrating to v4.0","text":"

This page covers every breaking change introduced in v4.0 and the steps required to update your code.

","path":["Migration","Migrating to v4.0"],"tags":[]},{"location":"migration/v4/#database","level":2,"title":"Database","text":"","path":["Migration","Migrating to v4.0"],"tags":[]},{"location":"migration/v4/#lock_tables-now-takes-a-session_maker-instead-of-a-session","level":3,"title":"lock_tables now takes a session_maker instead of a session","text":"

The first argument of lock_tables changed from an AsyncSession instance to an async_sessionmaker. The function creates and manages its own dedicated session internally, yielding it to the caller.

Before (v3)Now (v4)
from fastapi_toolsets.db import lock_tables, LockMode\n\nasync with lock_tables(session=session, tables=[User, Account]):\n    user = await UserCrud.get(session, [User.id == 1])\n    user.balance += 100\n\n# With a custom lock mode\nasync with lock_tables(session=session, tables=[Order], mode=LockMode.EXCLUSIVE):\n    await process_order(session, order_id)\n
from fastapi_toolsets.db import lock_tables, LockMode\n\nasync with lock_tables(session_maker=session_maker, tables=[User, Account]) as session:\n    user = await UserCrud.get(session, [User.id == 1])\n    user.balance += 100\n\n# With a custom lock mode\nasync with lock_tables(\n    session_maker=session_maker, tables=[Order], mode=LockMode.EXCLUSIVE\n) as session:\n    await process_order(session, order_id)\n
","path":["Migration","Migrating to v4.0"],"tags":[]},{"location":"migration/v5/","level":1,"title":"Migrating to v5.0","text":"

This page covers every breaking change introduced in v5.0 and the steps required to update your code.

","path":["Migration","Migrating to v5.0"],"tags":[]},{"location":"migration/v5/#database","level":2,"title":"Database","text":"

db.py is now the db/ package, built around one object, Database, that owns the engine and sessionmaker. The free functions that took a session_maker you built and passed around yourself are gone from request-handling code; Database builds the sessionmaker for you.

","path":["Migration","Migrating to v5.0"],"tags":[]},{"location":"migration/v5/#create_db_dependency-create_db_context-removed-in-favor-of-database","level":3,"title":"create_db_dependency / create_db_context removed in favor of Database","text":"

Build one Database with your URL (or an existing engine=), then use the instance directly as the FastAPI dependency, and db.session() for sessions outside request handlers.

Before (v4)Now (v5)
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker\nfrom fastapi_toolsets.db import create_db_dependency, create_db_context\n\nengine = create_async_engine(\"postgresql+asyncpg://...\")\nSessionLocal = async_sessionmaker(engine, expire_on_commit=False)\n\nget_db = create_db_dependency(session_maker=SessionLocal)\nget_db_context = create_db_context(session_maker=SessionLocal)\n\n\n@app.get(\"/users\")\nasync def list_users(session: AsyncSession = Depends(get_db)): ...\n\n\nasync def seed():\n    async with get_db_context() as session:\n        ...\n
from fastapi_toolsets.db import Database\n\ndb = Database(url=\"postgresql+asyncpg://...\")\n\n\n@app.get(\"/users\")\nasync def list_users(session: AsyncSession = Depends(db)): ...\n\n\nasync def seed():\n    async with db.session() as session:\n        ...\n

Call db.install(app) to also commit before the response is sent (instead of in dependency teardown) and to dispose the engine on shutdown. See the db module docs.

","path":["Migration","Migrating to v5.0"],"tags":[]},{"location":"migration/v5/#get_transaction-renamed-to-transaction","level":3,"title":"get_transaction renamed to transaction","text":"

Same behavior (savepoint when already in a transaction, new transaction otherwise), new name, same import path.

Before (v4)Now (v5)
from fastapi_toolsets.db import get_transaction\n\nasync with get_transaction(session=session):\n    session.add(model)\n
from fastapi_toolsets.db import transaction\n\nasync with transaction(session=session):\n    session.add(model)\n

If you have a Database instance, db.begin() opens a session already inside a transaction:

async with db.begin() as session:\n    session.add(User(name=\"ada\"))\n
","path":["Migration","Migrating to v5.0"],"tags":[]},{"location":"migration/v5/#lock_tables-is-now-also-a-database-method","level":3,"title":"lock_tables is now also a Database method","text":"

The free lock_tables(session_maker, tables, ...) function still exists for callers who manage their own session factory, but prefer db.lock_tables(tables, ...), which drops the session_maker argument:

Before (v4)Now (v5)
from fastapi_toolsets.db import lock_tables, LockMode\n\nasync with lock_tables(\n    session_maker=session_maker, tables=[Order], mode=LockMode.EXCLUSIVE\n) as session:\n    await process_order(session, order_id)\n
from fastapi_toolsets.db import LockMode\n\nasync with db.lock_tables(tables=[Order], mode=LockMode.EXCLUSIVE) as session:\n    await process_order(session, order_id)\n
","path":["Migration","Migrating to v5.0"],"tags":[]},{"location":"migration/v5/#create_database-and-cleanup_tables-moved-to-fastapi_toolsetsdbtesting","level":3,"title":"create_database and cleanup_tables moved to fastapi_toolsets.db.testing","text":"Before (v4)Now (v5)
from fastapi_toolsets.db import create_database, cleanup_tables\n
from fastapi_toolsets.db.testing import create_database, cleanup_tables\n
","path":["Migration","Migrating to v5.0"],"tags":[]},{"location":"migration/v5/#fixtures","level":2,"title":"Fixtures","text":"","path":["Migration","Migrating to v5.0"],"tags":[]},{"location":"migration/v5/#get_obj_by_attr-get_field_by_attr-are-now-fixtureregistry-methods","level":3,"title":"get_obj_by_attr / get_field_by_attr are now FixtureRegistry methods","text":"

Both also change their first argument: instead of the fixture function, pass the fixture's registered name and let the registry look it up.

Before (v4)Now (v5)
from fastapi_toolsets.fixtures import get_obj_by_attr, get_field_by_attr\n\n\n@fixtures.register(depends_on=[\"roles\"])\ndef users():\n    admin_role = get_obj_by_attr(fixtures=roles, attr_name=\"name\", value=\"admin\")\n    admin_role_id = get_field_by_attr(fixtures=roles, attr_name=\"name\", value=\"admin\")\n    return [User(id=1, username=\"alice\", role_id=admin_role.id)]\n
@fixtures.register(depends_on=[\"roles\"])\ndef users():\n    admin_role = fixtures.obj(name=\"roles\", attr_name=\"name\", value=\"admin\")\n    admin_role_id = fixtures.field(name=\"roles\", attr_name=\"name\", value=\"admin\")\n    return [User(id=1, username=\"alice\", role_id=admin_role.id)]\n
","path":["Migration","Migrating to v5.0"],"tags":[]},{"location":"migration/v5/#loadinglisting-by-context-now-always-includes-contextbase","level":3,"title":"Loading/listing by context now always includes Context.BASE","text":"

FixtureRegistry.get_variants/get_by_context, load_fixtures_by_context, and the fixtures list/fixtures load CLI commands now implicitly union every queried context with Context.BASE. In v4, requesting a single context (e.g. \"testing\") returned only fixtures tagged with that context; in v5 it also returns every Context.BASE-tagged fixture.

If you relied on strict exclusion of base fixtures, move them out of Context.BASE into their own context.

","path":["Migration","Migrating to v5.0"],"tags":[]},{"location":"migration/v5/#load_fixtures-load_fixtures_by_context-now-return-reloaded-instances","level":3,"title":"load_fixtures / load_fixtures_by_context now return reloaded instances","text":"

Loaded objects are re-queried from the database after insert, with all relationships eager-loaded, instead of returning the exact objects your fixture function constructed. Code that compares returned objects by identity to the fixture function's output, or that expected relationships to remain unloaded, should be updated — and expect one extra query per model type per load.

","path":["Migration","Migrating to v5.0"],"tags":[]},{"location":"migration/v5/#security","level":2,"title":"Security","text":"

The security module has been removed and moved to a dedicated python package: fastapi-multiauth.

Run uv add fastapi-multiauth and replace from fastapi_toolsets.security import ... with from fastapi_multiauth import ....

","path":["Migration","Migrating to v5.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]\ncustom_cli = \"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/#fixtures-load","level":3,"title":"fixtures load","text":"
manager fixtures load [CONTEXTS]... [--strategy merge|insert|skip_existing] [--dry-run]\n

CONTEXTS defaults to Context.BASE when omitted, and can also be set via the FIXTURES_CONTEXT environment variable, handy for CI/deploy scripts that shouldn't need an explicit argument per environment:

FIXTURES_CONTEXT=testing manager fixtures load\n

An explicit CLI argument always takes precedence over the environment variable.

","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\n@cli.command()\ndef hello():\n    print(\"Hello from my app!\")\n
[tool.fastapi-toolsets]\ncustom_cli = \"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, a 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":"","path":["Modules","CRUD"],"tags":[]},{"location":"module/crud/#factory-style","level":3,"title":"Factory style","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. This is the most concise option for straightforward CRUD with no custom logic.

","path":["Modules","CRUD"],"tags":[]},{"location":"module/crud/#subclass-style","level":3,"title":"Subclass style","text":"

Added in v2.3.0

from fastapi_toolsets.crud.factory import AsyncCrud\nfrom myapp.models import User\n\n\nclass UserCrud(AsyncCrud[User]):\n    model = User\n    searchable_fields = [User.username, User.email]\n    default_load_options = [selectinload(User.role)]\n

Subclassing AsyncCrud directly is the preferred style when you need to add custom methods or when the configuration is complex enough to benefit from a named class body.

","path":["Modules","CRUD"],"tags":[]},{"location":"module/crud/#adding-custom-methods","level":3,"title":"Adding custom methods","text":"
class UserCrud(AsyncCrud[User]):\n    model = User\n\n    @classmethod\n    async def get_active(cls, session: AsyncSession) -> list[User]:\n        return await cls.get_multi(session, filters=[User.is_active == True])\n
","path":["Modules","CRUD"],"tags":[]},{"location":"module/crud/#sharing-a-custom-base-across-multiple-models","level":3,"title":"Sharing a custom base across multiple models","text":"

Define a generic base class with the shared methods, then subclass it for each model:

from typing import Generic, TypeVar\nfrom sqlalchemy.ext.asyncio import AsyncSession\nfrom sqlalchemy.orm import DeclarativeBase\nfrom fastapi_toolsets.crud.factory import AsyncCrud\n\nT = TypeVar(\"T\", bound=DeclarativeBase)\n\n\nclass AuditedCrud(AsyncCrud[T], Generic[T]):\n    \"\"\"Base CRUD with custom function\"\"\"\n\n    @classmethod\n    async def get_active(cls, session: AsyncSession):\n        return await cls.get_multi(session, filters=[cls.model.is_active == True])\n\n\nclass UserCrud(AuditedCrud[User]):\n    model = User\n    searchable_fields = [User.username, User.email]\n

You can also use the factory shorthand with the same base by passing base_class:

UserCrud = CrudFactory(User, base_class=AuditedCrud)\n
","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(\n    session=session, obj=UserUpdateSchema(username=\"bob\"), filters=[User.id == user_id]\n)\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/#row-locking","level":2,"title":"Row locking","text":"

get, get_or_none, first, get_multi, and update all accept a with_for_update parameter that appends a FOR UPDATE clause to the underlying SELECT, preventing concurrent transactions from modifying the matched rows until the current transaction commits.

Value SQL clause False (default) no locking True FOR UPDATE \"nowait\" FOR UPDATE NOWAIT \"skip_locked\" FOR UPDATE SKIP LOCKED
# Lock before reading — typical read-modify-write pattern\nuser = await UserCrud.get(session, [User.id == user_id], with_for_update=True)\n\n# Raise immediately if another transaction holds the lock\nuser = await UserCrud.get(session, [User.id == user_id], with_for_update=\"nowait\")\n\n# Skip rows already locked by another transaction (e.g. job queues)\nrows = await JobCrud.get_multi(\n    session, filters=[Job.status == \"pending\"], with_for_update=\"skip_locked\"\n)\n\n# Lock atomically as part of update (prevents race between SELECT and UPDATE)\nuser = await UserCrud.update(\n    session, UserUpdate(credits=10), [User.id == user_id], with_for_update=True\n)\n

Warning

with_for_update requires an open transaction. Wrap your call in async with session.begin() or use the transaction helper if you are not already inside one.

Note

NOWAIT raises sqlalchemy.exc.OperationalError immediately if the row is locked rather than waiting.

","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)

Three pagination methods are available. All return a typed response whose pagination_type field tells clients which strategy was used.

offset_paginate cursor_paginate paginate Return type OffsetPaginatedResponse CursorPaginatedResponse either, based on pagination_type param Total count Yes No / Jump to arbitrary page Yes No / Performance on deep pages Degrades Constant / Stable under concurrent inserts No Yes / Use case Admin panels, numbered pagination Feeds, APIs, infinite scroll single endpoint, both strategies","path":["Modules","CRUD"],"tags":[]},{"location":"module/crud/#offset-pagination","level":3,"title":"Offset pagination","text":"
from typing import Annotated\nfrom fastapi import Depends\n\n\n@router.get(\"\")\nasync def get_users(\n    session: SessionDep,\n    params: Annotated[dict, Depends(UserCrud.offset_paginate_params())],\n) -> OffsetPaginatedResponse[UserRead]:\n    return await UserCrud.offset_paginate(session=session, **params, schema=UserRead)\n

The offset_paginate method returns an OffsetPaginatedResponse:

{\n  \"status\": \"SUCCESS\",\n  \"pagination_type\": \"offset\",\n  \"data\": [\"...\"],\n  \"pagination\": {\n    \"total_count\": 100,\n    \"pages\": 5,\n    \"page\": 1,\n    \"items_per_page\": 20,\n    \"has_more\": true\n  }\n}\n
","path":["Modules","CRUD"],"tags":[]},{"location":"module/crud/#skipping-the-count-query","level":4,"title":"Skipping the COUNT query","text":"

Added in v2.4.1

By default offset_paginate runs two queries: one for the page items and one COUNT(*) for total_count. On large tables the COUNT can be expensive. Pass include_total=False to offset_paginate_params() to skip it:

@router.get(\"\")\nasync def get_users(\n    session: SessionDep,\n    params: Annotated[\n        dict, Depends(UserCrud.offset_paginate_params(include_total=False))\n    ],\n) -> OffsetPaginatedResponse[UserRead]:\n    return await UserCrud.offset_paginate(session=session, **params, schema=UserRead)\n
","path":["Modules","CRUD"],"tags":[]},{"location":"module/crud/#cursor-pagination","level":3,"title":"Cursor pagination","text":"
@router.get(\"\")\nasync def list_users(\n    session: SessionDep,\n    params: Annotated[dict, Depends(UserCrud.cursor_paginate_params())],\n) -> CursorPaginatedResponse[UserRead]:\n    return await UserCrud.cursor_paginate(session=session, **params, schema=UserRead)\n

The cursor_paginate method returns a CursorPaginatedResponse:

{\n  \"status\": \"SUCCESS\",\n  \"pagination_type\": \"cursor\",\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 URL-safe base64-encoded (no padding) 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/#unified-endpoint-both-strategies","level":3,"title":"Unified endpoint (both strategies)","text":"

Added in v2.3.0

paginate() dispatches to offset_paginate or cursor_paginate based on a pagination_type query parameter, letting you expose one endpoint that supports both strategies. The pagination_type field in the response tells clients which strategy was used, enabling frontend discriminated-union typing.

from fastapi_toolsets.schemas import PaginatedResponse\n\n\n@router.get(\"\")\nasync def list_users(\n    session: SessionDep,\n    params: Annotated[dict, Depends(UserCrud.paginate_params())],\n) -> PaginatedResponse[UserRead]:\n    return await UserCrud.paginate(session, **params, schema=UserRead)\n
GET /users?pagination_type=offset&page=2&items_per_page=10\nGET /users?pagination_type=cursor&cursor=eyJ2YWx1ZSI6...&items_per_page=10\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

Or via the dependency to narrow which fields are exposed as query parameters:

params = UserCrud.offset_paginate_params(search_fields=[Post.title])\n

This allows searching with both offset_paginate and cursor_paginate:

@router.get(\"\")\nasync def get_users(\n    session: SessionDep,\n    params: Annotated[dict, Depends(UserCrud.offset_paginate_params())],\n) -> OffsetPaginatedResponse[UserRead]:\n    return await UserCrud.offset_paginate(session=session, **params, schema=UserRead)\n
@router.get(\"\")\nasync def get_users(\n    session: SessionDep,\n    params: Annotated[dict, Depends(UserCrud.cursor_paginate_params())],\n) -> CursorPaginatedResponse[UserRead]:\n    return await UserCrud.cursor_paginate(session=session, **params, schema=UserRead)\n

The dependency adds two query parameters to the endpoint:

Parameter Type search str \\| null search_column str \\| null
GET /posts?search=hello                        → search all configured columns\nGET /posts?search=hello&search_column=title    → search only Post.title\n

The available search column keys are returned in the search_columns field of PaginatedResponse. Use them to populate a column picker in the UI, or to validate search_column values on the client side:

{\n  \"status\": \"SUCCESS\",\n  \"data\": [\"...\"],\n  \"pagination\": { \"...\" },\n  \"search_columns\": [\"content\", \"author__username\", \"title\"]\n}\n

Key format uses __ as a separator for relationship chains.

A direct column Post.title produces \"title\". A relationship tuple (Post.author, User.username) produces \"author__username\". An unknown search_column value raises InvalidSearchColumnError (HTTP 422).

","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. Relationship traversal is supported via tuples, using the same syntax as searchable_fields:

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

Or via the dependency to narrow which fields are exposed as query parameters:

params = UserCrud.offset_paginate_params(facet_fields=[User.country])\n

Facet filtering is built into the consolidated params dependencies. When filter=True (the default), each facet field is exposed as a query parameter and values are collected into filter_by automatically:

from typing import Annotated\n\nfrom fastapi import Depends\n\n\n@router.get(\"\", response_model_exclude_none=True)\nasync def list_users(\n    session: SessionDep,\n    params: Annotated[dict, Depends(UserCrud.offset_paginate_params())],\n) -> OffsetPaginatedResponse[UserRead]:\n    return await UserCrud.offset_paginate(session=session, **params, schema=UserRead)\n
@router.get(\"\", response_model_exclude_none=True)\nasync def list_users(\n    session: SessionDep,\n    params: Annotated[dict, Depends(UserCrud.cursor_paginate_params())],\n) -> CursorPaginatedResponse[UserRead]:\n    return await UserCrud.cursor_paginate(session=session, **params, schema=UserRead)\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__name=admin&role__name=editor → filter_by={\"role__name\": [\"admin\", \"editor\"]}  (IN clause)\n

filter_by and filters can be combined — both are applied with AND logic.

The distinct values for each facet field are returned in the filter_attributes field of PaginatedResponse. Use them to populate filter dropdowns in the UI, or to validate filter_by keys on the client side:

{\n  \"status\": \"SUCCESS\",\n  \"data\": [\"...\"],\n  \"pagination\": { \"...\" },\n  \"filter_attributes\": {\n    \"status\": [\"active\", \"inactive\"],\n    \"country\": [\"DE\", \"FR\", \"US\"],\n    \"role__name\": [\"admin\", \"editor\", \"viewer\"]\n  }\n}\n

Key format uses __ as a separator for relationship chains.

A direct column User.status produces \"status\". A relationship tuple (User.role, Role.name) produces \"role__name\". A deeper chain (User.role, Role.permission, Permission.name) produces \"role__permission__name\". An unknown filter_by key raises InvalidFacetFilterError (HTTP 422).

","path":["Modules","CRUD"],"tags":[]},{"location":"module/crud/#skipping-facet-queries","level":4,"title":"Skipping facet queries","text":"

Added in v5.1.0

Facet values only change with the filters, not with the page. Pass include_facets=False to offset_paginate_params() / cursor_paginate_params() on pages 2..N to skip the facet queries entirely (filter_attributes will be None):

params: Annotated[dict, Depends(UserCrud.offset_paginate_params(include_facets=False))]\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. Relationship traversal is supported via tuples, using the same syntax as searchable_fields and facet_fields:

UserCrud = CrudFactory(\n    model=User,\n    order_fields=[\n        User.name,\n        User.created_at,\n        (User.role, Role.name),  # sort by a related model column\n    ],\n)\n

You can override order_fields per call:

result = await UserCrud.offset_paginate(\n    session=session,\n    order_fields=[User.name],\n)\n

Or via the dependency to narrow which fields are exposed as query parameters:

params = UserCrud.offset_paginate_params(order_fields=[User.name])\n

Sorting is built into the consolidated params dependencies. When order=True (the default), order_by and order query parameters are exposed and resolved into an OrderByClause automatically:

from typing import Annotated\n\nfrom fastapi import Depends\n\n\n@router.get(\"\")\nasync def list_users(\n    session: SessionDep,\n    params: Annotated[dict, Depends(UserCrud.offset_paginate_params())],\n) -> OffsetPaginatedResponse[UserRead]:\n    return await UserCrud.offset_paginate(session=session, **params, schema=UserRead)\n
@router.get(\"\")\nasync def list_users(\n    session: SessionDep,\n    params: Annotated[dict, Depends(UserCrud.cursor_paginate_params())],\n) -> CursorPaginatedResponse[UserRead]:\n    return await UserCrud.cursor_paginate(session=session, **params, schema=UserRead)\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=role__name&order=desc  → LEFT JOIN roles ON ... ORDER BY roles.name DESC\n

Relationship tuples are joined automatically.

When a relation field is selected, the related table is LEFT OUTER JOINed automatically. An unknown order_by value raises InvalidOrderFieldError (HTTP 422).

The available sort keys are returned in the order_columns field of PaginatedResponse. Use them to populate a sort picker in the UI, or to validate order_by values on the client side:

{\n  \"status\": \"SUCCESS\",\n  \"data\": [\"...\"],\n  \"pagination\": { \"...\" },\n  \"order_columns\": [\"created_at\", \"name\", \"role__name\"]\n}\n

Key format uses __ as a separator for relationship chains.

A direct column User.name produces \"name\". A relationship tuple (User.role, Role.name) produces \"role__name\".

","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(\n    session=session, obj=PostCreateSchema(title=\"Hello\", tag_ids=[1, 2, 3])\n)\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\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\n@router.get(\"\")\nasync def list_users(\n    session: SessionDep,\n    params: Annotated[dict, Depends(crud.UserCrud.offset_paginate_params())],\n) -> OffsetPaginatedResponse[UserRead]:\n    return await crud.UserCrud.offset_paginate(\n        session=session, **params, 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, advisory 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 is built around one object, Database, which owns the engine and sessionmaker and exposes the FastAPI dependency, a commit-before-response middleware, session/transaction context managers, and table locking. Free helpers cover savepoint-aware transactions, advisory locks, many-to-many association tables, and row-change polling.

","path":["Modules","DB"],"tags":[]},{"location":"module/db/#setup","level":2,"title":"Setup","text":"

Create one Database for your app. Provide a URL (the facade builds and disposes the engine) or pass an existing engine= you own (e.g. for Alembic or event.listen). The session factory is built internally with expire_on_commit=False.

from fastapi import Depends, FastAPI\nfrom sqlalchemy.ext.asyncio import AsyncSession\n\nfrom fastapi_toolsets.db import Database\n\ndb = Database(\"postgresql+asyncpg://postgres:postgres@localhost/app\")\n\napp = FastAPI()\ndb.install(app)  # commit middleware + engine disposal on shutdown\n\n\n@app.get(\"/users\")\nasync def list_users(session: AsyncSession = Depends(db)): ...\n

The Database instance is the dependency: use it directly as Depends(db). The whole request runs as a single transaction (CRUD writes use savepoints under it).

The URL may be a plain string or a Pydantic PostgresDsn. In URL mode you can tune the engine: pass connect_args for DBAPI-level options and any other keyword for create_async_engine (e.g. pool_size, echo, pool_pre_ping):

from pydantic_settings import BaseSettings\nfrom pydantic import PostgresDsn\n\n\nclass Settings(BaseSettings):\n    database_url: PostgresDsn\n\n\nsettings = Settings()\n\ndb = Database(\n    settings.database_url,\n    pool_size=20,\n    pool_pre_ping=True,\n    connect_args={\"server_settings\": {\"application_name\": \"myapp\"}},\n)\n
","path":["Modules","DB"],"tags":[]},{"location":"module/db/#committing-before-the-response","level":2,"title":"Committing before the response","text":"

db.install(app) adds a middleware that commits the request's session when the response starts, after the endpoint returns and before the body is sent. With the middleware installed, the dependency does not commit again.

The request is committed as a single transaction:

  • Read-after-write: a follow-up request sees the write.
  • Atomicity: multi-write endpoints roll back as a unit on failure.
  • Errors roll back: on a raised exception the session rolls back and nothing is committed.

Without install, the session commits in the dependency teardown, which runs after the response has been sent.

Streaming / SSE endpoints

For a StreamingResponse / EventSourceResponse, the commit fires at the start of the stream. A stream that writes must open a short-lived session per write with db.session(); the start-time commit will not flush writes made later during the stream.

","path":["Modules","DB"],"tags":[]},{"location":"module/db/#lifespan","level":2,"title":"Lifespan","text":"

db.install(app) disposes the engine on shutdown, composing around your own lifespan:

from contextlib import asynccontextmanager\n\n\n@asynccontextmanager\nasync def lifespan(app):\n    await warm_cache()  # your startup\n    yield\n    await flush_metrics()  # your shutdown\n\n\napp = FastAPI(lifespan=lifespan)\ndb.install(app)  # your shutdown runs first, then the engine is disposed\n

If you have no lifespan of your own, db.lifespan works standalone as FastAPI(lifespan=db.lifespan). Engine disposal is idempotent and is a no-op when you passed your own engine=.

","path":["Modules","DB"],"tags":[]},{"location":"module/db/#session-context-manager","level":2,"title":"Session context manager","text":"

Use db.session() for sessions outside request handlers (e.g. background tasks, CLI commands). It commits on clean exit and rolls back on exception:

async def seed():\n    async with db.session() as session:\n        ...\n
","path":["Modules","DB"],"tags":[]},{"location":"module/db/#transactions","level":2,"title":"Transactions","text":"

transaction opens a transaction on a session, using a savepoint when one is already open so it nests safely:

from fastapi_toolsets.db import transaction\n\n\nasync def create_user_with_role(session):\n    async with transaction(session):\n        ...\n        async with transaction(session):  # uses a savepoint\n            ...\n

When you have a Database, db.begin() opens a session already inside a transaction:

async with db.begin() as session:\n    session.add(User(name=\"ada\"))  # commits on exit, rolls back on exception\n
","path":["Modules","DB"],"tags":[]},{"location":"module/db/#table-locking","level":2,"title":"Table locking","text":"

db.lock_tables acquires PostgreSQL table-level locks for a critical section. It opens a dedicated session internally and releases the lock when the context exits:

from fastapi_toolsets.db import LockMode\n\nasync with db.lock_tables([User], mode=LockMode.EXCLUSIVE) as session:\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.

Pass timeout to limit how long the lock waits. On timeout, a LockTimeoutError is raised instead of a raw database error:

async with db.lock_tables([Order], timeout=\"2s\") as session:\n    ...\n
","path":["Modules","DB"],"tags":[]},{"location":"module/db/#advisory-locking","level":2,"title":"Advisory locking","text":"

advisory_lock acquires a PostgreSQL session-level advisory lock on a session you provide. The lock is released when the context exits:

from fastapi_toolsets.db import advisory_lock\n\n# Blocking exclusive lock: waits until the lock is free\nasync with advisory_lock(session=session, key=42):\n    ...\n\n# Non-blocking: yields False immediately if already held\nasync with advisory_lock(session=session, key=42, nowait=True) as acquired:\n    if not acquired:\n        raise HTTPException(409, \"Resource is locked\")\n\n# Blocking with a timeout: raises LockTimeoutError if not acquired in time\nasync with advisory_lock(session=session, key=42, timeout=\"5s\"):\n    ...\n\n# Shared lock: multiple readers allowed simultaneously, blocks exclusive writers\nasync with advisory_lock(session=session, key=42, shared=True):\n    ...\n\n# Two-integer key for namespacing (e.g. lock_type + resource_id)\nasync with advisory_lock(session=session, key=(1, user_id)):\n    ...\n

Note

Advisory locks use PostgreSQL session-level functions (pg_advisory_lock / pg_advisory_unlock). The lock is tied to the database connection, not the SQLAlchemy transaction, so it is released when the context exits even if the surrounding transaction is still open.

","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:

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=[\"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":"

create_database (in fastapi_toolsets.db.testing) connects to server_url and issues a CREATE DATABASE statement:

from fastapi_toolsets.db.testing 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, which handles drop-before, create, and drop-after.

","path":["Modules","DB"],"tags":[]},{"location":"module/db/#cleaning-up-tables","level":2,"title":"Cleaning up tables","text":"

cleanup_tables (in fastapi_toolsets.db.testing) truncates all tables:

from fastapi_toolsets.db.testing import cleanup_tables\n\n\n@pytest.fixture(autouse=True)\nasync def clean(db_session):\n    yield\n    await cleanup_tables(session=db_session, base=Base)\n
","path":["Modules","DB"],"tags":[]},{"location":"module/db/#many-to-many-helpers","level":2,"title":"Many-to-Many helpers","text":"

The three m2m_* helpers modify a many-to-many association table with direct SQL, without loading the ORM collection.

","path":["Modules","DB"],"tags":[]},{"location":"module/db/#m2m_add-insert-associations","level":3,"title":"m2m_add: insert associations","text":"

m2m_add inserts one or more rows into a secondary table:

from fastapi_toolsets.db import m2m_add\n\nasync with db.lock_tables([Tag]) as session:\n    tag = await TagCrud.create(session, TagCreate(name=\"python\"))\n    await m2m_add(session, post, Post.tags, tag)\n

Pass ignore_conflicts=True to skip associations that already exist:

await m2m_add(session, post, Post.tags, tag, ignore_conflicts=True)\n
","path":["Modules","DB"],"tags":[]},{"location":"module/db/#m2m_remove-delete-associations","level":3,"title":"m2m_remove: delete associations","text":"

m2m_remove deletes specific association rows. Removing a non-existent association is a no-op:

from fastapi_toolsets.db import m2m_remove, transaction\n\nasync with transaction(session):\n    await m2m_remove(session, post, Post.tags, tag1, tag2)\n
","path":["Modules","DB"],"tags":[]},{"location":"module/db/#m2m_set-replace-the-full-set","level":3,"title":"m2m_set: replace the full set","text":"

m2m_set replaces all associations: it deletes every existing row for the owner instance then inserts the new set. Passing no related instances clears the association:

from fastapi_toolsets.db import m2m_set, transaction\n\n# Replace all tags\nasync with transaction(session):\n    await m2m_set(session, post, Post.tags, tag_a, tag_b)\n\n# Clear all tags\nasync with transaction(session):\n    await m2m_set(session, post, Post.tags)\n

All three helpers raise TypeError if the relationship attribute is not a Many-to-Many (i.e. has no secondary table).

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\n# Plain callable\nUserDep = PathDependency(model=User, field=User.id, session_dep=get_db)\n\n# Annotated\nSessionDep = Annotated[AsyncSession, Depends(get_db)]\nUserDep = PathDependency(model=User, field=User.id, session_dep=SessionDep)\n\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\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\n# Plain callable\nRoleDep = BodyDependency(\n    model=Role, field=Role.id, session_dep=get_db, body_field=\"role_id\"\n)\n\n# Annotated\nSessionDep = Annotated[AsyncSession, Depends(get_db)]\nRoleDep = BodyDependency(\n    model=Role, field=Role.id, session_dep=SessionDep, body_field=\"role_id\"\n)\n\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 PoolExhaustedError 503 Service Unavailable LockTimeoutError 503 Service Unavailable","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(\n    detail=\"User 42 not found\", desc=\"No user with that ID exists in the database.\"\n)\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\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\n\nclass PaymentRequiredError(BillingError):\n    api_error = ApiError(\n        code=402, msg=\"Payment Required\", desc=\"...\", err_code=\"BILLING-402\"\n    )\n\n\nclass SubscriptionExpiredError(BillingError):\n    api_error = ApiError(\n        code=402, msg=\"Subscription Expired\", desc=\"...\", err_code=\"BILLING-402-EXP\"\n    )\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\n@fixtures.register\ndef roles():\n    return [\n        Role(id=1, name=\"admin\"),\n        Role(id=2, name=\"user\"),\n    ]\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, fixtures, Context.TESTING)\n

Directly by name with load_fixtures:

from fastapi_toolsets.fixtures import load_fixtures\n\nasync with db_context() as session:\n    await load_fixtures(session, fixtures, \"roles\", \"test_users\")\n

Both functions return a dict[str, list[...]] mapping each fixture name to the list of loaded instances.

","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.DEVELOPMENT Data only loaded in development Context.PRODUCTION Data only loaded in production

A fixture with no contexts defined takes Context.BASE by default.

Context.BASE fixtures are always included alongside whatever context you load or list — there's no way to load a non-base context in isolation:

# also loads any Context.BASE fixtures, even though only TESTING is requested\nawait load_fixtures_by_context(session, fixtures, Context.TESTING)\n
","path":["Modules","Fixtures"],"tags":[]},{"location":"module/fixtures/#custom-contexts","level":3,"title":"Custom contexts","text":"

Plain strings and any Enum subclass are accepted wherever a Context enum is expected.

from enum import Enum\n\n\nclass AppContext(str, Enum):\n    STAGING = \"staging\"\n    DEMO = \"demo\"\n\n\n@fixtures.register(contexts=[AppContext.STAGING])\ndef staging_data():\n    return [Config(key=\"feature_x\", enabled=True)]\n\n\n# loads staging_data plus any Context.BASE fixtures\nawait load_fixtures_by_context(session, fixtures, AppContext.STAGING)\n
","path":["Modules","Fixtures"],"tags":[]},{"location":"module/fixtures/#default-context-for-a-registry","level":3,"title":"Default context for a registry","text":"

Pass contexts to FixtureRegistry to set a default for all fixtures registered in it:

testing_registry = FixtureRegistry(contexts=[Context.TESTING])\n\n\n@testing_registry.register  # implicitly contexts=[Context.TESTING]\ndef test_orders():\n    return [Order(id=1, total=99)]\n
","path":["Modules","Fixtures"],"tags":[]},{"location":"module/fixtures/#same-fixture-name-multiple-context-variants","level":3,"title":"Same fixture name, multiple context variants","text":"

The same fixture name may be registered under different (non-overlapping) context sets. When multiple contexts are loaded together, all matching variants are merged:

@fixtures.register(contexts=[Context.BASE])\ndef users():\n    return [User(id=1, username=\"admin\")]\n\n\n@fixtures.register(contexts=[Context.TESTING])\ndef users():\n    return [User(id=2, username=\"tester\")]\n\n\n# loads both admin and tester (Context.BASE is included automatically)\nawait load_fixtures_by_context(session, fixtures, Context.TESTING)\n

Registering two variants with overlapping context sets raises ValueError.

","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.MERGE Insert or update on conflict (default) LoadStrategy.SKIP_EXISTING Skip rows that already exist
await load_fixtures_by_context(\n    session, fixtures, Context.BASE, strategy=LoadStrategy.SKIP_EXISTING\n)\n
","path":["Modules","Fixtures"],"tags":[]},{"location":"module/fixtures/#merging-registries","level":2,"title":"Merging registries","text":"

Split fixture definitions across modules and merge them:

from myapp.fixtures.dev import dev_fixtures\nfrom myapp.fixtures.prod import prod_fixtures\n\nfixtures = FixtureRegistry()\nfixtures.include_registry(registry=dev_fixtures)\nfixtures.include_registry(registry=prod_fixtures)\n

Fixtures with the same name are allowed as long as their context sets do not overlap. Conflicting contexts raise ValueError.

","path":["Modules","Fixtures"],"tags":[]},{"location":"module/fixtures/#looking-up-fixture-instances","level":2,"title":"Looking up fixture instances","text":"

FixtureRegistry.obj retrieves a specific instance from a registered fixture by attribute value, looked up by name on the registry — useful when building cross-fixture depends_on relationships:

@fixtures.register(depends_on=[\"roles\"])\ndef users():\n    admin_role = fixtures.obj(\"roles\", \"name\", \"admin\")\n    return [User(id=1, username=\"alice\", role_id=admin_role.id)]\n

Looking the fixture up by name (instead of importing the roles function directly) means fixture modules never need to import each other, which avoids circular imports in larger projects split across multiple files — the same reason depends_on takes fixture names rather than the functions themselves. The registry passed in must be the one that actually contains the fixture by load time; with a single shared registry this is automatic, but if you merge registries with include_registry, call obj/field on the merged registry.

FixtureRegistry.field is shorthand for pulling a single attribute (id by default):

@fixtures.register(depends_on=[\"roles\"])\ndef users():\n    return [\n        User(id=1, username=\"alice\", role_id=fixtures.field(\"roles\", \"name\", \"admin\"))\n    ]\n

Both raise StopIteration if no matching instance is found, and KeyError if the fixture name isn't registered.

","path":["Modules","Fixtures"],"tags":[]},{"location":"module/fixtures/#pytest-integration","level":2,"title":"Pytest integration","text":"

Use register_fixtures to expose each fixture in your registry as an injectable pytest fixture named fixture_{name} by default:

# 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\n@pytest.fixture\nasync def db_session():\n    async with create_db_session(\n        database_url=DATABASE_URL, base=Base, cleanup=True\n    ) as session:\n        yield session\n\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

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\n@metrics.register\ndef http_requests():\n    return Counter(\"http_requests_total\", \"Total HTTP requests\", [\"method\", \"status\"])\n\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\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\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 (clock_timestamp()), 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(). The value is retrieved via RETURNING after insert, so it is available on the Python object immediately after flush().

Requires PostgreSQL 13+

from fastapi_toolsets.models import UUIDMixin\n\n\nclass User(Base, UUIDMixin):\n    __tablename__ = \"users\"\n\n    username: Mapped[str]\n\n\n# id is None before flush\nuser = User(username=\"alice\")\nsession.add(user)\nawait session.flush()\nprint(user.id)  # UUID('...')\n
","path":["Modules","Models"],"tags":[]},{"location":"module/models/#uuidv7mixin","level":3,"title":"UUIDv7Mixin","text":"

Added in v2.3

Adds a id: UUID primary key generated server-side by PostgreSQL using uuidv7(). It's a time-ordered UUID format that encodes a millisecond-precision timestamp in the most significant bits, making it naturally sortable and index-friendly.

Requires PostgreSQL 18+

from fastapi_toolsets.models import UUIDv7Mixin\n\n\nclass Event(Base, UUIDv7Mixin):\n    __tablename__ = \"events\"\n\n    name: Mapped[str]\n\n\n# id is None before flush\nevent = Event(name=\"user.signup\")\nsession.add(event)\nawait session.flush()\nprint(event.id)  # UUID('019...')\n
","path":["Modules","Models"],"tags":[]},{"location":"module/models/#createdatmixin","level":3,"title":"CreatedAtMixin","text":"

Adds a created_at: datetime column set to clock_timestamp() 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\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 clock_timestamp() on insert and automatically updated to clock_timestamp() on every ORM-level update (via SQLAlchemy's onupdate hook).

from fastapi_toolsets.models import UUIDMixin, UpdatedAtMixin\n\n\nclass Post(Base, UUIDMixin, UpdatedAtMixin):\n    __tablename__ = \"posts\"\n\n    title: Mapped[str]\n\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\n\nclass Article(Base, UUIDMixin, TimestampMixin):\n    __tablename__ = \"articles\"\n\n    title: Mapped[str]\n
","path":["Modules","Models"],"tags":[]},{"location":"module/models/#lifecycle-events","level":2,"title":"Lifecycle events","text":"

The event system provides lifecycle callbacks that fire after commit. If the transaction rolls back, no callback fires.

","path":["Modules","Models"],"tags":[]},{"location":"module/models/#setup","level":3,"title":"Setup","text":"

Event dispatch requires EventSession. Pass it as the session class when creating your session factory:

from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine\nfrom fastapi_toolsets.models import EventSession\n\nengine = create_async_engine(\"postgresql+asyncpg://...\")\nSessionLocal = async_sessionmaker(engine, expire_on_commit=False, class_=EventSession)\n

Callbacks fire on session.commit() only — not on savepoints.

Savepoints created by transaction or begin_nested() do not trigger callbacks. All events accumulated across flushes are dispatched once when the outermost commit() is called.

","path":["Modules","Models"],"tags":[]},{"location":"module/models/#events","level":3,"title":"Events","text":"

Three event types are available, each corresponding to a ModelEvent value:

Event Trigger ModelEvent.CREATE After INSERT commit ModelEvent.DELETE After DELETE commit ModelEvent.UPDATE After UPDATE commit on a watched field

Callbacks fire only for ORM-level changes. Rows updated via raw SQL (UPDATE ... SET ...) are not detected.

","path":["Modules","Models"],"tags":[]},{"location":"module/models/#watched-fields","level":3,"title":"Watched fields","text":"

Set __watched_fields__ on the model to restrict which field changes trigger UPDATE events. It must be a tuple[str, ...] — any other type raises TypeError:

Class attribute UPDATE behaviour __watched_fields__ = (\"status\", \"role\") Only fires when status or role changes (not set) Fires when any mapped field changes

__watched_fields__ is inherited through the class hierarchy via normal Python MRO. A subclass can override it:

class Order(Base, UUIDMixin):\n    __watched_fields__ = (\"status\",)\n    ...\n\n\nclass UrgentOrder(Order):\n    # inherits __watched_fields__ = (\"status\",)\n    ...\n\n\nclass PriorityOrder(Order):\n    __watched_fields__ = (\"priority\",)\n    # overrides parent — UPDATE fires only for priority changes\n    ...\n
","path":["Modules","Models"],"tags":[]},{"location":"module/models/#registering-handlers","level":3,"title":"Registering handlers","text":"

Register handlers with the listens_for decorator. Every callback receives three arguments: the model instance, the ModelEvent that triggered it, and a changes dict (None for CREATE and DELETE):

from fastapi_toolsets.models import ModelEvent, UUIDMixin, listens_for\n\n\nclass Order(Base, UUIDMixin):\n    __tablename__ = \"orders\"\n    __watched_fields__ = (\"status\",)\n\n    status: Mapped[str]\n\n\n@listens_for(Order, [ModelEvent.CREATE])\nasync def on_order_created(order: Order, event_type: ModelEvent, changes: None):\n    await notify_new_order(order.id)\n\n\n@listens_for(Order, [ModelEvent.DELETE])\nasync def on_order_deleted(order: Order, event_type: ModelEvent, changes: None):\n    await notify_order_cancelled(order.id)\n\n\n@listens_for(Order, [ModelEvent.UPDATE])\nasync def on_order_updated(order: Order, event_type: ModelEvent, changes: dict):\n    if \"status\" in changes:\n        await notify_status_change(order.id, changes[\"status\"])\n

Multiple handlers can be registered for the same model and event. Handlers registered on a parent class also fire for subclass instances.

A single handler can listen for multiple events at once. When event_types is omitted, the handler fires for all events:

@listens_for(Order, [ModelEvent.CREATE, ModelEvent.UPDATE])\nasync def on_order_changed(order: Order, event_type: ModelEvent, changes: dict | None):\n    await invalidate_cache(order.id)\n\n\n@listens_for(Order)  # all events\nasync def on_any_order_event(\n    order: Order, event_type: ModelEvent, changes: dict | None\n):\n    await audit_log(order.id, event_type)\n
","path":["Modules","Models"],"tags":[]},{"location":"module/models/#field-changes-format","level":3,"title":"Field changes format","text":"

The changes dict maps each watched field that changed to {\"old\": ..., \"new\": ...}. Only fields that actually changed are included. For CREATE and DELETE events, changes is None:

# CREATE / DELETE → changes is None\n# status changed   → {\"status\": {\"old\": \"pending\", \"new\": \"shipped\"}}\n# two fields changed → {\"status\": {...}, \"assigned_to\": {...}}\n

Multiple flushes in one transaction are merged: the earliest old and latest new are preserved, and on_update fires only once per commit.

API Reference

","path":["Modules","Models"],"tags":[]},{"location":"module/pytest/","level":1,"title":"Pytest","text":"

Testing helpers for FastAPI applications: async HTTP 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/#async-client","level":2,"title":"Async client","text":"

Use create_async_client to get an httpx.AsyncClient bound to your FastAPI app:

from fastapi_toolsets.pytest import create_async_client\n\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

Any extra keyword arguments are forwarded to httpx.AsyncClient, so you can set default headers, authentication, timeouts, and more:

async with create_async_client(\n    app=app,\n    headers={\"X-Api-Key\": \"secret\"},\n    timeout=10,\n) as c:\n    ...\n
","path":["Modules","Pytest"],"tags":[]},{"location":"module/pytest/#database-sessions","level":2,"title":"Database sessions","text":"

Use create_worker_database + create_db_session to get a fully isolated AsyncSession for each test:

from fastapi_toolsets.pytest import create_worker_database, create_db_session\n\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

create_worker_database connects without specifying a database (asyncpg falls back to the username), so the target test database does not need to exist beforehand.

Info

cleanup=True truncates all tables between tests via TRUNCATE … RESTART IDENTITY CASCADE, which is faster than dropping and recreating tables.

","path":["Modules","Pytest"],"tags":[]},{"location":"module/pytest/#engine-and-session-options","level":3,"title":"Engine and session options","text":"

Pass engine_kwargs or session_kwargs to forward options to the underlying SQLAlchemy primitives:

async with create_db_session(\n    database_url=worker_db_url,\n    base=Base,\n    engine_kwargs={\"pool_size\": 5, \"connect_args\": {\"timeout\": 10}},\n    session_kwargs={\"autoflush\": False},\n) as session:\n    ...\n
","path":["Modules","Pytest"],"tags":[]},{"location":"module/pytest/#parallel-testing-with-pytest-xdist","level":2,"title":"Parallel testing with pytest-xdist","text":"

The fixtures above work with pytest-xdist out of the box. Each worker gets its own database named after the worker (e.g. gw0, gw1). Pass prefix to namespace the database (e.g. prefix=\"myapp\"myapp_gw0).

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(\n    \"postgresql+asyncpg://user:pass@localhost/myapp\", default_test_db=\"test\"\n)\n# → \"postgresql+asyncpg://user:pass@localhost/gw0\" under xdist\n# → \"postgresql+asyncpg://user:pass@localhost/test\" otherwise\n\nurl = worker_database_url(\n    \"postgresql+asyncpg://user:pass@localhost/myapp\",\n    default_test_db=\"test\",\n    prefix=\"myapp\",\n)\n# → \"postgresql+asyncpg://user:pass@localhost/myapp_gw0\" under xdist\n# → \"postgresql+asyncpg://user:pass@localhost/myapp_test\" otherwise\n
","path":["Modules","Pytest"],"tags":[]},{"location":"module/pytest/#manual-table-cleanup","level":2,"title":"Manual table cleanup","text":"

cleanup_tables truncates all tables in a single statement and can be called directly when you need more control:

from fastapi_toolsets.pytest import cleanup_tables\n\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\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/#paginated-response-models","level":3,"title":"Paginated response models","text":"

Three classes wrap paginated list results. Pick the one that matches your endpoint's strategy:

Class pagination type pagination_type field Use when OffsetPaginatedResponse[T] OffsetPagination \"offset\" (fixed) endpoint always uses offset CursorPaginatedResponse[T] CursorPagination \"cursor\" (fixed) endpoint always uses cursor PaginatedResponse[T] OffsetPagination \\| CursorPagination — unified endpoint supporting both strategies","path":["Modules","Schemas"],"tags":[]},{"location":"module/schemas/#offsetpaginatedresponset","level":4,"title":"OffsetPaginatedResponse[T]","text":"

Added in v2.3.0

Use as the return type when the endpoint always uses offset_paginate. The pagination field is guaranteed to be an OffsetPagination object; the response always includes a pagination_type: \"offset\" discriminator.

from fastapi_toolsets.schemas import OffsetPaginatedResponse\n\n\n@router.get(\"/users\")\nasync def list_users(\n    page: int = 1,\n    items_per_page: int = 20,\n) -> OffsetPaginatedResponse[UserSchema]:\n    return await UserCrud.offset_paginate(\n        session, page=page, items_per_page=items_per_page, schema=UserSchema\n    )\n

Response shape:

{\n  \"status\": \"SUCCESS\",\n  \"pagination_type\": \"offset\",\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","Schemas"],"tags":[]},{"location":"module/schemas/#cursorpaginatedresponset","level":4,"title":"CursorPaginatedResponse[T]","text":"

Added in v2.3.0

Use as the return type when the endpoint always uses cursor_paginate. The pagination field is guaranteed to be a CursorPagination object; the response always includes a pagination_type: \"cursor\" discriminator.

from fastapi_toolsets.schemas import CursorPaginatedResponse\n\n\n@router.get(\"/events\")\nasync def list_events(\n    cursor: str | None = None,\n    items_per_page: int = 20,\n) -> CursorPaginatedResponse[EventSchema]:\n    return await EventCrud.cursor_paginate(\n        session, cursor=cursor, items_per_page=items_per_page, schema=EventSchema\n    )\n

Response shape:

{\n  \"status\": \"SUCCESS\",\n  \"pagination_type\": \"cursor\",\n  \"data\": [\"...\"],\n  \"pagination\": {\n    \"next_cursor\": \"eyJpZCI6IDQyfQ==\",\n    \"prev_cursor\": null,\n    \"items_per_page\": 20,\n    \"has_more\": true\n  }\n}\n
","path":["Modules","Schemas"],"tags":[]},{"location":"module/schemas/#paginatedresponset","level":4,"title":"PaginatedResponse[T]","text":"

Return type for endpoints that support both pagination strategies via a pagination_type query parameter (using paginate()).

When used as a return annotation, PaginatedResponse[T] automatically expands to Annotated[Union[CursorPaginatedResponse[T], OffsetPaginatedResponse[T]], Field(discriminator=\"pagination_type\")], so FastAPI emits a proper oneOf + discriminator in the OpenAPI schema with no extra boilerplate:

from fastapi_toolsets.crud import PaginationType\nfrom fastapi_toolsets.schemas import PaginatedResponse\n\n\n@router.get(\"/users\")\nasync def list_users(\n    pagination_type: PaginationType = PaginationType.OFFSET,\n    page: int = 1,\n    cursor: str | None = None,\n    items_per_page: int = 20,\n) -> PaginatedResponse[UserSchema]:\n    return await UserCrud.paginate(\n        session,\n        pagination_type=pagination_type,\n        page=page,\n        cursor=cursor,\n        items_per_page=items_per_page,\n        schema=UserSchema,\n    )\n
","path":["Modules","Schemas"],"tags":[]},{"location":"module/schemas/#pagination-metadata-models","level":4,"title":"Pagination metadata models","text":"

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\n
get_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 (\n    SearchConfig,\n    get_searchable_fields,\n    build_search_filters,\n)\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]\n
create(\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, order_joins=None, items_per_page=20, search=None, search_fields=None, search_column=None, order_fields=None, facet_fields=None, include_facets=True, 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 search_column str | None

Restrict search to a single column key.

None order_fields Sequence[OrderFieldType] | None

Fields allowed for sorting (overrides class default).

None facet_fields Sequence[FacetFieldType] | None

Columns to compute distinct values for (overrides class default).

None include_facets bool

When False, skip facet queries entirely; filter_attributes will be None.

True 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 CursorPaginatedResponse[Any]

PaginatedResponse with CursorPagination metadata

","path":["Reference","crud"],"tags":[]},{"location":"reference/crud/#fastapi_toolsets.crud.factory.AsyncCrud.cursor_paginate_params","level":3,"title":"cursor_paginate_params(*, default_page_size=20, max_page_size=100, include_facets=True, search=True, filter=True, order=True, search_fields=None, facet_fields=None, order_fields=None, default_order_field=None, default_order='asc') classmethod","text":"

Return a FastAPI dependency that collects all params for :meth:cursor_paginate.

Parameters:

Name Type Description Default default_page_size int

Default items_per_page value.

20 max_page_size int

Maximum items_per_page value.

100 include_facets bool

Whether to run facet queries (not a query param).

True search bool

Enable search query parameters.

True filter bool

Enable facet filter query parameters.

True order bool

Enable order query parameters.

True search_fields Sequence[SearchFieldType] | None

Override searchable fields.

None facet_fields Sequence[FacetFieldType] | None

Override facet fields.

None order_fields Sequence[OrderFieldType] | None

Override order fields.

None default_order_field QueryableAttribute[Any] | None

Default field to order by when order_by is absent.

None default_order Literal['asc', 'desc']

Default sort direction.

'asc'

Returns:

Name Type Description Callable[..., Awaitable[dict[str, Any]]]

An async dependency that resolves to a dict ready to be unpacked

into Callable[..., Awaitable[dict[str, Any]]]

meth:cursor_paginate.

","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]\n
delete(\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.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: _ForUpdateMode = False,\n    load_options: Sequence[ExecutableOption] | None = None,\n    schema: type[SchemaType],\n) -> Response[SchemaType] | None\n
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: _ForUpdateMode = 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 _ForUpdateMode

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: _ForUpdateMode = False,\n    load_options: Sequence[ExecutableOption] | None = None,\n    schema: type[SchemaType],\n) -> Response[SchemaType]\n
get(\n    session: AsyncSession,\n    filters: list[Any],\n    *,\n    joins: JoinType | None = None,\n    outer_join: bool = False,\n    with_for_update: _ForUpdateMode = 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 _ForUpdateMode

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, with_for_update=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 with_for_update _ForUpdateMode

Lock rows for update. True for plain FOR UPDATE, \"nowait\" for FOR UPDATE NOWAIT, \"skip_locked\" for FOR UPDATE SKIP LOCKED.

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: _ForUpdateMode = False,\n    load_options: Sequence[ExecutableOption] | None = None,\n    schema: type[SchemaType],\n) -> Response[SchemaType] | None\n
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: _ForUpdateMode = 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 _ForUpdateMode

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, order_joins=None, page=1, items_per_page=20, include_total=True, search=None, search_fields=None, search_column=None, order_fields=None, facet_fields=None, include_facets=True, 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 include_total bool

When False, skip the COUNT query; pagination.total_count will be None.

True search str | SearchConfig | None

Search query string or SearchConfig object

None search_fields Sequence[SearchFieldType] | None

Fields to search in (overrides class default)

None search_column str | None

Restrict search to a single column key.

None order_fields Sequence[OrderFieldType] | None

Fields allowed for sorting (overrides class default).

None facet_fields Sequence[FacetFieldType] | None

Columns to compute distinct values for (overrides class default)

None include_facets bool

When False, skip facet queries entirely; filter_attributes will be None. Useful on pages 2..N where the facet counts were already fetched on page 1.

True 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 OffsetPaginatedResponse[Any]

PaginatedResponse with OffsetPagination metadata

","path":["Reference","crud"],"tags":[]},{"location":"reference/crud/#fastapi_toolsets.crud.factory.AsyncCrud.offset_paginate_params","level":3,"title":"offset_paginate_params(*, default_page_size=20, max_page_size=100, include_total=True, include_facets=True, search=True, filter=True, order=True, search_fields=None, facet_fields=None, order_fields=None, default_order_field=None, default_order='asc') classmethod","text":"

Return a FastAPI dependency that collects all params for :meth:offset_paginate.

Parameters:

Name Type Description Default default_page_size int

Default items_per_page value.

20 max_page_size int

Maximum items_per_page value.

100 include_total bool

Whether to include total count (not a query param).

True include_facets bool

Whether to run facet queries (not a query param).

True search bool

Enable search query parameters.

True filter bool

Enable facet filter query parameters.

True order bool

Enable order query parameters.

True search_fields Sequence[SearchFieldType] | None

Override searchable fields.

None facet_fields Sequence[FacetFieldType] | None

Override facet fields.

None order_fields Sequence[OrderFieldType] | None

Override order fields.

None default_order_field QueryableAttribute[Any] | None

Default field to order by when order_by is absent.

None default_order Literal['asc', 'desc']

Default sort direction.

'asc'

Returns:

Name Type Description Callable[..., Awaitable[dict[str, Any]]]

An async dependency that resolves to a dict ready to be unpacked

into Callable[..., Awaitable[dict[str, Any]]]

meth:offset_paginate.

","path":["Reference","crud"],"tags":[]},{"location":"reference/crud/#fastapi_toolsets.crud.factory.AsyncCrud.paginate","level":3,"title":"paginate(session, *, pagination_type=PaginationType.OFFSET, filters=None, joins=None, outer_join=False, load_options=None, order_by=None, order_joins=None, page=1, cursor=None, items_per_page=20, include_total=True, search=None, search_fields=None, search_column=None, order_fields=None, facet_fields=None, include_facets=True, filter_by=None, schema) async classmethod","text":"
paginate(\n    session: AsyncSession,\n    *,\n    pagination_type: Literal[PaginationType.OFFSET],\n    filters: list[Any] | None = ...,\n    joins: JoinType | None = ...,\n    outer_join: bool = ...,\n    load_options: Sequence[ExecutableOption] | None = ...,\n    order_by: OrderByClause | None = ...,\n    order_joins: list[Any] | None = ...,\n    page: int = ...,\n    cursor: str | None = ...,\n    items_per_page: int = ...,\n    include_total: bool = ...,\n    search: str | SearchConfig | None = ...,\n    search_fields: Sequence[SearchFieldType] | None = ...,\n    search_column: str | None = ...,\n    order_fields: Sequence[OrderFieldType] | None = ...,\n    facet_fields: Sequence[FacetFieldType] | None = ...,\n    include_facets: bool = ...,\n    filter_by: dict[str, Any] | BaseModel | None = ...,\n    schema: type[BaseModel],\n) -> OffsetPaginatedResponse[Any]\n
paginate(\n    session: AsyncSession,\n    *,\n    pagination_type: Literal[PaginationType.CURSOR],\n    filters: list[Any] | None = ...,\n    joins: JoinType | None = ...,\n    outer_join: bool = ...,\n    load_options: Sequence[ExecutableOption] | None = ...,\n    order_by: OrderByClause | None = ...,\n    order_joins: list[Any] | None = ...,\n    page: int = ...,\n    cursor: str | None = ...,\n    items_per_page: int = ...,\n    include_total: bool = ...,\n    search: str | SearchConfig | None = ...,\n    search_fields: Sequence[SearchFieldType] | None = ...,\n    search_column: str | None = ...,\n    order_fields: Sequence[OrderFieldType] | None = ...,\n    facet_fields: Sequence[FacetFieldType] | None = ...,\n    include_facets: bool = ...,\n    filter_by: dict[str, Any] | BaseModel | None = ...,\n    schema: type[BaseModel],\n) -> CursorPaginatedResponse[Any]\n

Get paginated results using either offset or cursor pagination.

Parameters:

Name Type Description Default session AsyncSession

DB async session.

required pagination_type PaginationType

Pagination strategy. Defaults to PaginationType.OFFSET.

OFFSET 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

Column or expression to order results by.

None page int

Page number (1-indexed). Only used when pagination_type is OFFSET.

1 cursor str | None

Cursor token from a previous :class:.CursorPaginatedResponse. Only used when pagination_type is CURSOR.

None items_per_page int

Number of items per page (default 20).

20 include_total bool

When False, skip the COUNT query; only applies when pagination_type is OFFSET.

True search str | SearchConfig | None

Search query string or :class:.SearchConfig object.

None search_fields Sequence[SearchFieldType] | None

Fields to search in (overrides class default).

None search_column str | None

Restrict search to a single column key.

None order_fields Sequence[OrderFieldType] | None

Fields allowed for sorting (overrides class default).

None facet_fields Sequence[FacetFieldType] | None

Columns to compute distinct values for (overrides class default).

None include_facets bool

When False, skip facet queries entirely; filter_attributes will be None.

True 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 :exc:.InvalidFacetFilterError for unknown keys.

None schema type[BaseModel]

Pydantic schema to serialize each item into.

required

Returns:

Type Description OffsetPaginatedResponse[Any] | CursorPaginatedResponse[Any]

class:.OffsetPaginatedResponse when pagination_type is

OffsetPaginatedResponse[Any] | CursorPaginatedResponse[Any]

OFFSET, :class:.CursorPaginatedResponse when it is

OffsetPaginatedResponse[Any] | CursorPaginatedResponse[Any]

CURSOR.

","path":["Reference","crud"],"tags":[]},{"location":"reference/crud/#fastapi_toolsets.crud.factory.AsyncCrud.paginate_params","level":3,"title":"paginate_params(*, default_page_size=20, max_page_size=100, default_pagination_type=PaginationType.OFFSET, include_total=True, include_facets=True, search=True, filter=True, order=True, search_fields=None, facet_fields=None, order_fields=None, default_order_field=None, default_order='asc') classmethod","text":"

Return a FastAPI dependency that collects all params for :meth:paginate.

Parameters:

Name Type Description Default default_page_size int

Default items_per_page value.

20 max_page_size int

Maximum items_per_page value.

100 default_pagination_type PaginationType

Default pagination strategy.

OFFSET include_total bool

Whether to include total count (not a query param).

True include_facets bool

Whether to run facet queries (not a query param).

True search bool

Enable search query parameters.

True filter bool

Enable facet filter query parameters.

True order bool

Enable order query parameters.

True search_fields Sequence[SearchFieldType] | None

Override searchable fields.

None facet_fields Sequence[FacetFieldType] | None

Override facet fields.

None order_fields Sequence[OrderFieldType] | None

Override order fields.

None default_order_field QueryableAttribute[Any] | None

Default field to order by when order_by is absent.

None default_order Literal['asc', 'desc']

Default sort direction.

'asc'

Returns:

Name Type Description Callable[..., Awaitable[dict[str, Any]]]

An async dependency that resolves to a dict ready to be unpacked

into Callable[..., Awaitable[dict[str, Any]]]

meth:paginate.

","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, with_for_update=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    with_for_update: _ForUpdateMode = False,\n    schema: type[SchemaType],\n) -> Response[SchemaType]\n
update(\n    session: AsyncSession,\n    obj: BaseModel,\n    filters: list[Any],\n    *,\n    exclude_unset: bool = True,\n    exclude_none: bool = False,\n    with_for_update: _ForUpdateMode = 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 with_for_update _ForUpdateMode

Lock the row before updating. True for plain FOR UPDATE, \"nowait\" for FOR UPDATE NOWAIT, \"skip_locked\" for FOR UPDATE SKIP LOCKED.

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, *, base_class=AsyncCrud, 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 base_class type[AsyncCrud[Any]]

Optional base class to inherit from instead of AsyncCrud. Use this to share custom methods across multiple CRUD classes while still using the factory shorthand.

AsyncCrud 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[OrderFieldType] | None

Optional list of model attributes that callers are allowed to order by via offset_paginate_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\n# With a shared custom base class:\nfrom typing import Generic, TypeVar\nfrom sqlalchemy.orm import DeclarativeBase\n\nT = TypeVar(\"T\", bound=DeclarativeBase)\n\nclass AuditedCrud(AsyncCrud[T], Generic[T]):\n    @classmethod\n    async def get_active(cls, session):\n        return await cls.get_multi(session, filters=[cls.model.is_active == True])\n\nUserCrud = CrudFactory(User, base_class=AuditedCrud)\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, search_column=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 search_column str | None

Optional key to narrow search to a single field. Must match one of the resolved search field keys.

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 the Database facade, the transaction helper, locking functions, many-to-many helpers, and row-watching utilities.

You can import them directly from fastapi_toolsets.db:

from fastapi_toolsets.db import (\n    Database,\n    LockMode,\n    advisory_lock,\n    lock_tables,\n    m2m_add,\n    m2m_remove,\n    m2m_set,\n    transaction,\n    wait_for_row_change,\n)\n

Admin and test helpers live in fastapi_toolsets.db.testing:

from fastapi_toolsets.db.testing import cleanup_tables, create_database\n
","path":["Reference","db"],"tags":[]},{"location":"reference/db/#fastapi_toolsets.db.Database","level":2,"title":"fastapi_toolsets.db.Database","text":"

One object that owns the engine, sessions, dependency, and middleware.

Provide exactly one of url (the facade builds and disposes the engine) or engine (an engine you own, e.g. for Alembic or event.listen, left untouched).

Parameters:

Name Type Description Default url str | PostgresDsn | None

Database connection URL. Accepts a plain string or a Pydantic :class:~pydantic.PostgresDsn.

None engine AsyncEngine | None

An existing :class:AsyncEngine to reuse instead of url.

None session_class type[AsyncSession]

Session class for the sessionmaker (e.g. EventSession).

AsyncSession expire_on_commit bool

Expire attributes after commit. Defaults to False.

False autoflush bool

Autoflush the session before queries. Defaults to True.

True connect_args dict[str, Any] | None

DBAPI-level connection arguments forwarded to :func:create_async_engine (URL mode only).

None **engine_options Any

Extra keyword arguments forwarded to :func:create_async_engine (URL mode only).

{}

Raises:

Type Description TypeError

If neither or both of url and engine are given, or if connect_args/engine_options are passed together with engine.

Example
from fastapi import Depends, FastAPI\nfrom fastapi_toolsets.db import Database\n\ndb = Database(\"postgresql+asyncpg://postgres:postgres@localhost/app\")\n\napp = FastAPI()\ndb.install(app)\n\n@app.get(\"/users/{user_id}\")\nasync def get_user(user_id: int, session=Depends(db)):\n    return await UserCrud.get(session, [User.id == user_id])\n
","path":["Reference","db"],"tags":[]},{"location":"reference/db/#fastapi_toolsets.db.Database.__call__","level":3,"title":"__call__(request) async","text":"

FastAPI dependency: yield a session and commit once at the right time.

Parameters:

Name Type Description Default request Request

The incoming request (injected by FastAPI).

required

Yields:

Type Description AsyncGenerator[AsyncSession, None]

An AsyncSession for the duration of the request.

Example
@app.get(\"/users/{user_id}\")\nasync def get_user(user_id: int, session=Depends(db)):\n    return await UserCrud.get(session, [User.id == user_id])\n
","path":["Reference","db"],"tags":[]},{"location":"reference/db/#fastapi_toolsets.db.Database.begin","level":3,"title":"begin() async","text":"

Open a session already inside a transaction (sugar for the common case).

Equivalent to session() + :func:transaction. Commits on clean exit, rolls back on exception.

Yields:

Type Description AsyncGenerator[AsyncSession, None]

An AsyncSession open within a transaction.

Example
async with db.begin() as session:\n    session.add(User(name=\"ada\"))\n
","path":["Reference","db"],"tags":[]},{"location":"reference/db/#fastapi_toolsets.db.Database.install","level":3,"title":"install(app)","text":"

Wire the commit middleware and engine disposal onto app.

Parameters:

Name Type Description Default app Any

The FastAPI/Starlette application to wire.

required Example
@asynccontextmanager\nasync def lifespan(app):\n    ...  # your startup\n    yield\n    ...  # your shutdown\n\napp = FastAPI(lifespan=lifespan)\ndb.install(app)\n
","path":["Reference","db"],"tags":[]},{"location":"reference/db/#fastapi_toolsets.db.Database.lifespan","level":3,"title":"lifespan(app) async","text":"

Dispose the engine on shutdown; use as FastAPI(lifespan=db.lifespan).

Parameters:

Name Type Description Default app Any

The ASGI application (unused; required by the lifespan protocol).

required

Yields:

Type Description AsyncGenerator[None, None]

Control to the application for its lifetime.

Example
app = FastAPI(lifespan=db.lifespan)\n
","path":["Reference","db"],"tags":[]},{"location":"reference/db/#fastapi_toolsets.db.Database.lock_tables","level":3,"title":"lock_tables(tables, *, mode=LockMode.SHARE_UPDATE_EXCLUSIVE, timeout='5s')","text":"

Lock PostgreSQL tables for the duration of a dedicated transaction.

Opens its own session from the facade's sessionmaker, changes are committed when the context exits.

Parameters:

Name Type Description Default 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 AbstractAsyncContextManager[AsyncSession]

The dedicated session, open within the locked transaction.

Raises:

Type Description LockTimeoutError

If the lock cannot be acquired within timeout.

PoolExhaustedError

If the connection pool is exhausted.

Example
async with db.lock_tables([User, Account]) as session:\n    user = await UserCrud.get(session, [User.id == 1])\n    user.balance += 100\n
","path":["Reference","db"],"tags":[]},{"location":"reference/db/#fastapi_toolsets.db.Database.session","level":3,"title":"session() async","text":"

Open a session outside request handlers (background tasks, CLI, tests).

Commits on clean exit, rolls back on exception.

Yields:

Type Description AsyncGenerator[AsyncSession, None]

An AsyncSession ready for database operations.

Example
async with db.session() as session:\n    user = await UserCrud.get(session, [User.id == 1])\n
","path":["Reference","db"],"tags":[]},{"location":"reference/db/#fastapi_toolsets.db.transaction","level":2,"title":"fastapi_toolsets.db.transaction(session) async","text":"

Run a block inside a savepoint-aware transaction.

If session is already in a transaction, a nested transaction (savepoint) is opened so the block can roll back independently. Otherwise a top-level transaction is started. Commits on clean exit, rolls back on exception.

Parameters:

Name Type Description Default session AsyncSession

AsyncSession instance.

required

Yields:

Type Description AsyncGenerator[AsyncSession, None]

The session within the transaction context.

Example
from fastapi_toolsets.db import transaction\n\nasync with transaction(session):\n    session.add(model)\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.lock_tables","level":2,"title":"fastapi_toolsets.db.lock_tables(session_maker, tables, *, mode=LockMode.SHARE_UPDATE_EXCLUSIVE, timeout='5s')","text":"

Lock PostgreSQL tables for the duration of a transaction.

Prefer the method on a :class:Database instance; use this directly only when you manage your own session factory.

Parameters:

Name Type Description Default session_maker async_sessionmaker[_SessionT]

Async session factory used to create the dedicated session.

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 AbstractAsyncContextManager[_SessionT]

The dedicated session, open within the locked transaction.

Raises:

Type Description LockTimeoutError

If the lock cannot be acquired within timeout.

PoolExhaustedError

If the connection pool is exhausted.

Example
from fastapi_toolsets.db import lock_tables\n\nasync with lock_tables(session_maker, [User, Account]) as session:\n    user = await UserCrud.get(session, [User.id == 1])\n    user.balance += 100\n
","path":["Reference","db"],"tags":[]},{"location":"reference/db/#fastapi_toolsets.db.advisory_lock","level":2,"title":"fastapi_toolsets.db.advisory_lock(session, key, *, shared=False, nowait=False, timeout=None) async","text":"

Acquire a PostgreSQL session-level advisory lock.

Parameters:

Name Type Description Default session AsyncSession

AsyncSession instance.

required key int | tuple[int, int]

Lock key, either a single int (bigint) or a (int, int) pair for namespacing.

required shared bool

Acquire a shared lock (multiple holders allowed). Default is exclusive.

False nowait bool

Return False immediately if the lock is unavailable instead of waiting.

False timeout str | None

Maximum wait time (e.g. \"5s\", \"500ms\"). Raises DBAPIError if exceeded. Ignored when nowait is True.

None

Yields:

Type Description AsyncGenerator[bool, None]

True if the lock was acquired, False if nowait is True and the lock

AsyncGenerator[bool, None]

is already held.

Raises:

Type Description LockTimeoutError

If timeout is set and the lock cannot be acquired in time.

Example
from fastapi_toolsets.db import advisory_lock\n\nasync with advisory_lock(session, 42):\n    ...\n\nasync with advisory_lock(session, 42, nowait=True) as acquired:\n    if not acquired:\n        raise HTTPException(409, \"Resource is locked\")\n\nasync with advisory_lock(session, 42, timeout=\"5s\"):\n    ...\n\nasync with advisory_lock(session, (1, user_id), shared=True):\n    ...\n
","path":["Reference","db"],"tags":[]},{"location":"reference/db/#fastapi_toolsets.db.m2m_add","level":2,"title":"fastapi_toolsets.db.m2m_add(session, instance, rel_attr, *related, ignore_conflicts=False) async","text":"

Insert rows into a Many-to-Many association table without loading the ORM collection.

Parameters:

Name Type Description Default session AsyncSession

DB async session.

required instance DeclarativeBase

The \"owner\" side model instance (e.g. the A in A.b_list).

required rel_attr QueryableAttribute

The M2M relationship attribute on the model class (e.g. A.b_list).

required *related DeclarativeBase

One or more related instances to associate with instance.

() ignore_conflicts bool

When True, silently skip rows that already exist in the association table (ON CONFLICT DO NOTHING).

False

Raises:

Type Description TypeError

If rel_attr is not a Many-to-Many relationship.

Example
from fastapi_toolsets.db import m2m_add, transaction\n\nasync with transaction(session):\n    await m2m_add(session, post, Post.tags, tag1, tag2)\n
","path":["Reference","db"],"tags":[]},{"location":"reference/db/#fastapi_toolsets.db.m2m_remove","level":2,"title":"fastapi_toolsets.db.m2m_remove(session, instance, rel_attr, *related) async","text":"

Remove rows from a Many-to-Many association table without loading the ORM collection.

Parameters:

Name Type Description Default session AsyncSession

DB async session.

required instance DeclarativeBase

The \"owner\" side model instance (e.g. the A in A.b_list).

required rel_attr QueryableAttribute

The M2M relationship attribute on the model class (e.g. A.b_list).

required *related DeclarativeBase

One or more related instances to disassociate from instance.

()

Raises:

Type Description TypeError

If rel_attr is not a Many-to-Many relationship.

Example
from fastapi_toolsets.db import m2m_remove, transaction\n\nasync with transaction(session):\n    await m2m_remove(session, post, Post.tags, tag1)\n
","path":["Reference","db"],"tags":[]},{"location":"reference/db/#fastapi_toolsets.db.m2m_set","level":2,"title":"fastapi_toolsets.db.m2m_set(session, instance, rel_attr, *related) async","text":"

Replace the entire Many-to-Many association set atomically.

Parameters:

Name Type Description Default session AsyncSession

DB async session.

required instance DeclarativeBase

The \"owner\" side model instance (e.g. the A in A.b_list).

required rel_attr QueryableAttribute

The M2M relationship attribute on the model class (e.g. A.b_list).

required *related DeclarativeBase

The new complete set of related instances.

()

Raises:

Type Description TypeError

If rel_attr is not a Many-to-Many relationship.

Example
from fastapi_toolsets.db import m2m_set, transaction\n\nasync with transaction(session):\n    await m2m_set(session, post, Post.tags, tag1, tag2)  # replaces all\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.testing.create_database","level":2,"title":"fastapi_toolsets.db.testing.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.testing 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.testing.cleanup_tables","level":2,"title":"fastapi_toolsets.db.testing.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.

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    InvalidSearchColumnError,\n    InvalidFacetFilterError,\n    InvalidOrderFieldError,\n    PoolExhaustedError,\n    LockTimeoutError,\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.InvalidSearchColumnError","level":2,"title":"fastapi_toolsets.exceptions.exceptions.InvalidSearchColumnError","text":"

Bases: ApiException

Raised when search_column is not one of the configured searchable fields.

","path":["Reference","exceptions"],"tags":[]},{"location":"reference/exceptions/#fastapi_toolsets.exceptions.exceptions.InvalidSearchColumnError.__init__","level":3,"title":"__init__(column, valid_columns)","text":"

Initialize the exception.

Parameters:

Name Type Description Default column str

The unknown search column provided by the caller.

required valid_columns list[str]

List of valid search column keys.

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.PoolExhaustedError","level":2,"title":"fastapi_toolsets.exceptions.exceptions.PoolExhaustedError","text":"

Bases: ApiException

HTTP 503 - Database connection pool is exhausted.

","path":["Reference","exceptions"],"tags":[]},{"location":"reference/exceptions/#fastapi_toolsets.exceptions.exceptions.PoolExhaustedError.__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.LockTimeoutError","level":2,"title":"fastapi_toolsets.exceptions.exceptions.LockTimeoutError","text":"

Bases: ApiException

HTTP 503 - A database lock could not be acquired within the timeout.

","path":["Reference","exceptions"],"tags":[]},{"location":"reference/exceptions/#fastapi_toolsets.exceptions.exceptions.LockTimeoutError.__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.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)\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

Fixtures with the same name may be registered for different contexts. When multiple contexts are loaded together, their instances are merged:

```python\n@fixtures.register(contexts=[Context.BASE])\ndef users():\n    return [User(id=1, username=\"admin\")]\n\n@fixtures.register(contexts=[Context.TESTING])\ndef users():\n    return [User(id=2, username=\"tester\")]\n```\n
","path":["Reference","fixtures"],"tags":[]},{"location":"reference/fixtures/#fastapi_toolsets.fixtures.registry.FixtureRegistry.field","level":3,"title":"field(name, attr_name, value, *, field='id')","text":"

Get a single field value from a fixture object matched by an attribute.

Parameters:

Name Type Description Default name str

Fixture name to look up.

required attr_name str

Name of the attribute to match against.

required value Any

Value to match.

required field str

Attribute name to return from the matched object (default: \"id\").

'id'

Returns:

Type Description Any

The value of field on the first matching model instance.

Raises:

Type Description KeyError

If no fixture named name is registered.

StopIteration

If no matching object is found.

","path":["Reference","fixtures"],"tags":[]},{"location":"reference/fixtures/#fastapi_toolsets.fixtures.registry.FixtureRegistry.get","level":3,"title":"get(name)","text":"

Get a fixture by name.

Raises:

Type Description KeyError

If no fixture with name is registered.

ValueError

If the fixture has multiple context variants — use :meth:get_variants in that case.

","path":["Reference","fixtures"],"tags":[]},{"location":"reference/fixtures/#fastapi_toolsets.fixtures.registry.FixtureRegistry.get_all","level":3,"title":"get_all()","text":"

Get all registered fixtures (all variants of all names).

","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.get_dependencies","level":3,"title":"get_dependencies(name)","text":"

Get the union of depends_on across all variants of name.

Raises:

Type Description KeyError

If no fixture named name is registered.

","path":["Reference","fixtures"],"tags":[]},{"location":"reference/fixtures/#fastapi_toolsets.fixtures.registry.FixtureRegistry.get_load_variants","level":3,"title":"get_load_variants(name, *contexts)","text":"

Return variants for name filtered by contexts.

Raises:

Type Description KeyError

If no fixture with name is registered.

","path":["Reference","fixtures"],"tags":[]},{"location":"reference/fixtures/#fastapi_toolsets.fixtures.registry.FixtureRegistry.get_variants","level":3,"title":"get_variants(name, *contexts)","text":"

Return all registered variants for name, optionally filtered by context.

Parameters:

Name Type Description Default name str

Fixture name.

required *contexts str | Enum

If given, only return variants whose context set intersects with these values (:class:Context.BASE variants are always included). Both :class:Context enum values and plain strings are accepted.

()

Returns:

Type Description list[Fixture]

List of matching :class:Fixture objects (may be empty when a

list[Fixture]

context filter is applied and nothing matches).

Raises:

Type Description KeyError

If no fixture with name is registered.

","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.

Fixtures with the same name are allowed as long as their context sets do not overlap. Conflicting contexts raise :class:ValueError.

Parameters:

Name Type Description Default registry FixtureRegistry

The FixtureRegistry to include

required

Raises:

Type Description ValueError

If a fixture name already exists with overlapping contexts

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.obj","level":3,"title":"obj(name, attr_name, value)","text":"

Get a model instance from a registered fixture by attribute value.

Parameters:

Name Type Description Default name str

Fixture name to look up.

required attr_name str

Name of the attribute to match against.

required value Any

Value to match.

required

Returns:

Type Description DeclarativeBase

The first model instance where the attribute matches the given value.

Raises:

Type Description KeyError

If no fixture named name is registered.

StopIteration

If no matching object is found.

","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 | Enum] | None

List of contexts this fixture belongs to. Both :class:Context enum values and plain strings are accepted.

None Example

```python @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)]

","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 | Enum

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.

When a fixture name has multiple context variants, the union of all variants' depends_on lists is used.

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.

All context variants of each requested fixture are loaded and merged.

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

Contexts to load (e.g., Context.TESTING, or plain strings for custom contexts)

() 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/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    EventSession,\n    ModelEvent,\n    UUIDMixin,\n    UUIDv7Mixin,\n    CreatedAtMixin,\n    UpdatedAtMixin,\n    TimestampMixin,\n    listens_for,\n)\n
","path":["Reference","models"],"tags":[]},{"location":"reference/models/#fastapi_toolsets.models.EventSession","level":2,"title":"fastapi_toolsets.models.EventSession","text":"

Bases: AsyncSession

AsyncSession subclass that dispatches lifecycle callbacks after commit.

","path":["Reference","models"],"tags":[]},{"location":"reference/models/#fastapi_toolsets.models.ModelEvent","level":2,"title":"fastapi_toolsets.models.ModelEvent","text":"

Bases: str, Enum

Event types dispatched by :class:EventSession.

","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.UUIDv7Mixin","level":2,"title":"fastapi_toolsets.models.UUIDv7Mixin","text":"

Mixin that adds a UUIDv7 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/models/#fastapi_toolsets.models.listens_for","level":2,"title":"fastapi_toolsets.models.listens_for(model_class, event_types=None)","text":"

Register a callback for one or more model lifecycle events.

Parameters:

Name Type Description Default model_class type

The SQLAlchemy model class to listen on.

required event_types list[ModelEvent] | None

List of :class:ModelEvent values to listen for. Defaults to all event types.

None","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, **kwargs) 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 **kwargs Any

Additional keyword arguments forwarded to :class:httpx.AsyncClient (e.g. headers, cookies, auth, timeout).

{}

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, engine_kwargs=None, session_kwargs=None) 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 engine_kwargs dict[str, Any] | None

Additional keyword arguments forwarded to :func:sqlalchemy.ext.asyncio.create_async_engine (e.g. pool_size, connect_args).

None session_kwargs dict[str, Any] | None

Additional keyword arguments forwarded to :class:sqlalchemy.ext.asyncio.async_sessionmaker (e.g. autoflush, class_).

None

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, *, prefix=None)","text":"

Derive a per-worker database URL for pytest-xdist parallel runs.

Sets the database name to the worker name so each xdist worker operates on its own database. When not running under xdist, default_test_db is used instead. When prefix is provided, the name becomes {prefix}_{worker}.

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

Optional prefix prepended to the worker name (e.g. \"test\"\"test_gw0\"). Without it, the database name is just the worker name (e.g. \"gw0\").

None

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', *, prefix=None, server_url=None) 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 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' prefix str | None

Optional prefix prepended to the worker name (e.g. prefix=\"test\"\"test_gw0\"). Without it, the database name is just the worker name (e.g. \"gw0\").

None server_url str | None

URL used for server-level DDL (must point to an existing database on the same server). Defaults to database_url with the database omitted, letting asyncpg fall back to the username.

None

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/myapp\"\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    PaginationType,\n    PaginatedResponse,\n    OffsetPaginatedResponse,\n    CursorPaginatedResponse,\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 | None

Total number of items across all pages. None when include_total=False.

items_per_page int

Number of items per page

page int

Current page number (1-indexed)

has_more bool

Whether there are more pages

pages int | None

Total number of pages

","path":["Reference","schemas"],"tags":[]},{"location":"reference/schemas/#fastapi_toolsets.schemas.OffsetPagination.pages","level":3,"title":"pages property","text":"

Total number of pages, or None when total_count is unknown.

","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.PaginationType","level":2,"title":"fastapi_toolsets.schemas.PaginationType","text":"

Bases: str, Enum

Pagination strategy selector for :meth:.AsyncCrud.paginate.

","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.

Base class and return type for endpoints that support both pagination strategies. Use :class:OffsetPaginatedResponse or :class:CursorPaginatedResponse when the strategy is fixed.

When used as PaginatedResponse[T] in a return annotation, subscripting returns Annotated[Union[CursorPaginatedResponse[T], OffsetPaginatedResponse[T]], Field(discriminator=\"pagination_type\")] so FastAPI emits a proper oneOf + discriminator in the OpenAPI schema.

","path":["Reference","schemas"],"tags":[]},{"location":"reference/schemas/#fastapi_toolsets.schemas.OffsetPaginatedResponse","level":2,"title":"fastapi_toolsets.schemas.OffsetPaginatedResponse","text":"

Bases: PaginatedResponse[DataT]

Paginated response with typed offset-based pagination metadata.

The pagination_type field is always \"offset\" and acts as a discriminator, allowing frontend clients to narrow the union type returned by a unified paginate() endpoint.

","path":["Reference","schemas"],"tags":[]},{"location":"reference/schemas/#fastapi_toolsets.schemas.CursorPaginatedResponse","level":2,"title":"fastapi_toolsets.schemas.CursorPaginatedResponse","text":"

Bases: PaginatedResponse[DataT]

Paginated response with typed cursor-based pagination metadata.

The pagination_type field is always \"cursor\" and acts as a discriminator, allowing frontend clients to narrow the union type returned by a unified paginate() endpoint.

","path":["Reference","schemas"],"tags":[]}]} \ No newline at end of file diff --git a/v5.1/sitemap.xml b/v5.1/sitemap.xml new file mode 100644 index 0000000..2fb4397 --- /dev/null +++ b/v5.1/sitemap.xml @@ -0,0 +1,87 @@ + + + + https://fastapi-toolsets.d3vyce.fr/v5.1/ + + + https://fastapi-toolsets.d3vyce.fr/v5.1/module/cli/ + + + https://fastapi-toolsets.d3vyce.fr/v5.1/module/crud/ + + + https://fastapi-toolsets.d3vyce.fr/v5.1/module/db/ + + + https://fastapi-toolsets.d3vyce.fr/v5.1/module/dependencies/ + + + https://fastapi-toolsets.d3vyce.fr/v5.1/module/exceptions/ + + + https://fastapi-toolsets.d3vyce.fr/v5.1/module/fixtures/ + + + https://fastapi-toolsets.d3vyce.fr/v5.1/module/logger/ + + + https://fastapi-toolsets.d3vyce.fr/v5.1/module/metrics/ + + + https://fastapi-toolsets.d3vyce.fr/v5.1/module/models/ + + + https://fastapi-toolsets.d3vyce.fr/v5.1/module/pytest/ + + + https://fastapi-toolsets.d3vyce.fr/v5.1/module/schemas/ + + + https://fastapi-toolsets.d3vyce.fr/v5.1/reference/cli/ + + + https://fastapi-toolsets.d3vyce.fr/v5.1/reference/crud/ + + + https://fastapi-toolsets.d3vyce.fr/v5.1/reference/db/ + + + https://fastapi-toolsets.d3vyce.fr/v5.1/reference/dependencies/ + + + https://fastapi-toolsets.d3vyce.fr/v5.1/reference/exceptions/ + + + https://fastapi-toolsets.d3vyce.fr/v5.1/reference/fixtures/ + + + https://fastapi-toolsets.d3vyce.fr/v5.1/reference/logger/ + + + https://fastapi-toolsets.d3vyce.fr/v5.1/reference/metrics/ + + + https://fastapi-toolsets.d3vyce.fr/v5.1/reference/models/ + + + https://fastapi-toolsets.d3vyce.fr/v5.1/reference/pytest/ + + + https://fastapi-toolsets.d3vyce.fr/v5.1/reference/schemas/ + + + https://fastapi-toolsets.d3vyce.fr/v5.1/examples/pagination-search/ + + + https://fastapi-toolsets.d3vyce.fr/v5.1/migration/v5/ + + + https://fastapi-toolsets.d3vyce.fr/v5.1/migration/v4/ + + + https://fastapi-toolsets.d3vyce.fr/v5.1/migration/v3/ + + + https://fastapi-toolsets.d3vyce.fr/v5.1/migration/v2/ + + \ No newline at end of file diff --git a/versions.json b/versions.json index 1db6e30..519edf9 100644 --- a/versions.json +++ b/versions.json @@ -1,11 +1,16 @@ [ { - "version": "v5.0", - "title": "v5.0", + "version": "v5.1", + "title": "v5.1", "aliases": [ "stable" ] }, + { + "version": "v5.0", + "title": "v5.0", + "aliases": [] + }, { "version": "v4.1", "title": "v4.1",